4-3 Linux C++编译、链接与调试
1.编译与链接过程
在Linux系统使用gcc/g++编译C++程序,其过程可分为4个阶段:
1.1 预处理
在预处理阶段做三件事:、
- 1.删除注释;
- 2.处理源文件中的#ifdef、#include和#define预处理命令;
- 3.将包含的头文件展开;
可使用 gcc -E 生成预处理后的中间文件*.i。 例如,有如下代码:
#include <stdio.h>
#define MYNAME "Evila"
int main()
{
// print Hello Evila!
printf("Hello %s! \n", MYNAME);
return 0;
}
使用gcc -E main.c -o main.i
生成预处理后的文件,打开生成的main.i,可以看到<stdio.h>头文件展开,编写的注释被清理掉,宏在调用出被替换。
1.2 编译过程
编译阶段继续处理预处理阶段的产物,即main.i文件,编译期主要进行语法分析和词法分析,将符号汇总到符号表后生成汇编文件。 使用gcc -S main.i -o main.s
生成汇编文件:
符号表是编译期产生的一个hash列表,包括了变量、函数以及调试等信息,可以通过nm命令查看符号表。词法分析时向符号表里注册符号,当有重复定义的时候会报错,因为符号表已经存在该符号。
1.3 汇编阶段
汇编阶段继续处理编译阶段的产物,即main.s,汇编器会将汇编语言翻译成二进制的机器指令,并生成一份目标文件。 使用gcc -c main.s -o main.o
生成二进制目标文件,在Linux系统下以ELF文件的形式存储。一个gcc指令会生成一个二进制目标文件。如果某个代码工程中包含多个.h+.cpp文件时,会使用多个gcc指令进行编译,也生成了多个二进制目标文件。
1.3.1 ELF文件
ELF文件(Executable Linkable Format)是一种文件存储格式,Linux下的目标文件和可执行文件都按照该格式进行存储。ELF文件以按段(section)的形式进行分段存储,其中代码编译后的指令放在代码段(code section),未初始化的全局变量和局部静态变量放到bss区,已初始化的全局变量和局部静态变量放到data区。ELF的文件头记录了整个文件的属性信息,每个字段的含义可参考ELF文档。
使用readelf -h main.o
查看本例生成的main.o文件,可得:
$ readelf -h main.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0 // 程序的入口地址,这是没有链接的目标文件所以值是0x00
Start of program headers: 0 (bytes into file) // 程序头的起始位置(非可执行文件为0)
Start of section headers: 328 (bytes into file) // 段表开始位置的首字节
Flags: 0x0
Size of this header: 64 (bytes) // 当前ELF文件头的大小,这里是64字节
Size of program headers: 0 (bytes) // 程序头大小(非可执行文件为0)
Number of program headers: 0
Size of section headers: 64 (bytes) // 段表的长度(字节为单位)
Number of section headers: 13 // 段表中项数,也就是有多少段
Section header string table index: 10
1.3.2 段表及段(section)
ELF文件由各种各样的段组成,段表中记录了各个段的信息,以数组形式存放。段表的起始位置,长度,个数分别由ELF文件头中的Start of section headers,Size of section headers,Number of section headers指出。
使用readelf -S main.o
查看段表的详细信息如下:
There are 13 section headers, starting at offset 0x148:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000022 0000000000000000 AX 0 0 4
[ 2] .rela.text RELA 0000000000000000 000005a8
0000000000000048 0000000000000018 11 1 8
[ 3] .data PROGBITS 0000000000000000 00000064
0000000000000000 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00000064
0000000000000000 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 00000064
0000000000000011 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000075
000000000000002d 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000a2
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000a8
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000005f0
0000000000000018 0000000000000018 11 8 8
[10] .shstrtab STRTAB 0000000000000000 000000e0
0000000000000061 0000000000000000 0 0 1
[11] .symtab SYMTAB 0000000000000000 00000488
0000000000000108 0000000000000018 12 9 8
[12] .strtab STRTAB 0000000000000000 00000590
0000000000000014 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
main.o共有13个Section:
- .text段保存代码编译后的指令,使用
objdump -s -d main.o
查看.text段详细内容 - .data段保存已初始化的全局静态变量和局部静态变量,使用查看
objdump -x -s -d main.o
.data段内容。 - .rodata段存保存只读数据,包括const修饰的变量和字符串常量,使用查看
objdump -x -s -d main.o
.rodata段内容。 - .symtab段是符号表段,以数组结构保存符号信息(函数和变量的地址),例如(1)定义在目标文件中的全局符号,可以被其他目标文件引用;(2)在本目标文件中引用的全局符号,却不在本目标文件中定义,比如pritnf。使用
readelf -s main.o
查看目标文件的符号表。
1.4 链接阶段
在1.1节编写的例子中,只有一个main.c文件,目前我们已经生成了目标文件main.o。但是main.o可以直接执行吗?答案:不可以,因为在这个代码中并没有定义“printf”的函数实现,且在预处理中包含进去的“stdio.h”中也只有该函数的声明,而没有定义函数的实现。
那么是在哪里实现的“printf”函数的呢? 答:printf函数的实现被打包到libc.so.6库文件中,gcc会到系统默认的搜索路径“/usr/lib”下进行查找libc.so.6等库文件并执行链接。libc.so.6库避免了重复的开发printf等函数,使用时只需要include stdio.h
,并链接libc.so.6库到可执行文件即可。由于经过预处理、编译和汇编阶段生成的多个目标文件都是非可执行文件且是相互离散的,因此需要链接器将多个目标文件以及使用到的库文件进行合并,最终生成可执行文件。
链接器的主要工作可总结为以下4点: (1)合并各个目标文件的段内容; (2)调整合并后段的起始位置; (3)合并符号表,并进行符号分析; (4)进行符号重定位工作;
2.静态链接库
2.1 静态链接及其特点
静态链接是在链接期间将源文件使用到的库代码与编译生成的目标文件进行链接合并,生成的可执行文件。可执行文件是静态链接库与编译生成的目标文件集合,在运行时间与静态链接库再无关联。静态链接具有以下三个特点:
-
- 静态链接会增大空间和资源的消耗,因为所有相关的目标文件与函数库被链接合成一个可执行文件
-
- 如果静态库有升级或变动,可执行文件需要重新编译、链接
-
- 程序在运行时与函数库再无关联,运行时无关环境是否具有需要的库,方便移植
2.2 制作静态库(.a)
静态链接库通常以lib作为前缀、.a作为后缀。在Linux系统中使用ar命令将一组目标文件(.o)打包形成静态链接库,例如编写一个函数printName,它接受一个string参数,功能是输出这个string参数:
// comm_func.h
#include <string>
#include <iostream>
using std::string;
void printName(string name);
// comm_func.cpp
#include "comm_func.h"
void printName(string name)
{
std::cout<< " I am " << name << std::endl;
}
可按照如下步骤将其制作成静态链接库:
-
- 使用
gcc -c comm_func.cpp -o comm_func.o
命令将其编译为可重定向目标文件。注意,必须加-c,否则直接编译为可执行文件。
- 使用
-
- 使用
ar -rcs libcomm
命令将comm_func.o制作成静态库,静态库的命名规则是lib开头,.a结尾。
- 使用
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>