一、编译的基本过程
主要使用的工具是gcc,下面以一个简单的程序来演示基本的编译过程
1、源文件
- 本源文件意在展示编译过程,总共包含四个文件:主函数main.c、头文件vector.h、头文件实现addvec.c及multvec.c 。
- main.c
#include"vector.h"
int x[2] = {
1, 2};
int y[2] = {
3, 3};
int z[2];
int main()
{
addvec(x, y, z, 2);
return 0;
}
extern int addcnt;
extern int multcnt;
void addvec (int*,int*,int*,int);
void multvec (int*,int*,int*,int);
int addcnt = 0;
void addvec(int *x, int *y, int *z, int n)
{
int i;
addcnt++;
for (i = 0; i < n; ++i)
z[i] = x[i] + y[i];
}
int multcnt = 0;
void multvec(int *x, int *y, int *z, int n)
{
int i;
multcnt++;
for (i = 0; i < n; ++i)
z[i] = x[i] * y[i];
}
2、预处理文件
shell命令: gcc -E main.c -o main.i
- 预处理器cpp来完成预处理的工作,主要处理宏和include文件,常用选项如下:
- -D name[=definition] 相当于#define NAME 或 #define NAME 100等c程序中的宏定义
- -U name 相当于#undef
- -include file 顾名思义#include"file"
- -undef Do not predefine any system-specific or GCC-specific macros. The standard predefined macros remain defined
使用上面命令行会生成如下文件:
- main.i
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "main.c"
# 1 "vector.h" 1
extern int addcnt;
extern int multcnt;
void addvec (int*,int*,int*,int);
void multvec (int*,int*,int*,int);
# 2 "main.c" 2
int x[2] = {
1, 2};
int y[2] = {
3, 3};
int z[2];
int main()
{
addvec(x, y, z, 2);
return 0;
}
3、汇编文件
shell命令 gcc -S main.c -o main.s
- cc1程序将c语言程序转化为汇编代码程序,注意此处的数字使用的是十进制,全局变量x、y、z的地址还没有确定,调用的函数采用助记符标示。
上面shell命令行生成的文件如下,可以看出函数声明在其中好像没有体现,其实不然:函数声明会影响程序汇编代码的内容,函数调用之前的参数该如何传递,如果没有函数声明,传递参数的代码将无法实现汇编。
22 main:
23 .LFB0:
25 pushq %rbp
28 movq %rsp, %rbp
30 movl $2, %ecx
31 leaq z(%rip), %rdx
32 leaq y(%rip), %rsi
33 leaq x(%rip), %rdi
34 call addvec@PLT
35 movl $0, %eax
36 popq %rbp
38 ret
4、可重定位目标文件
shell命令: gcc -c mian.c && objdump -dx main.o
- as程序,将汇编代码转变成可重定位的目标文件;内容与汇编代码相同,但数字时十六进制、标明了需要重定位符号的重定位方式;另外指令的长度也确定了。
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: b9 02 00 00 00 mov $0x2,%ecx
9: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx
c: R_X86_64_PC32 z-0x4
10: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi
13: R_X86_64_PC32 y-0x4
17: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi
1a: R_X86_64_PC32 x-0x4
1e: e8 00 00 00 00 callq 23 <main+0x23>
1f: R_X86_64_PLT32 addvec-0x4
23: b8 00 00 00 00 mov $0x0,%eax
28: 5d pop %rbp
29: c3 retq
5、可执行文件
shell命令: gcc main.o addvec.o -o main && objdump -dx main
- ld程序负责链接工作,此处main.o跟addvec.o的链接方式是静态链接。
- 对比上面反汇编的可重定位目标文件及反汇编的可执行文件,可以看出:
1. main函数的代码在链接前后代码长度没有发生变化,操作码都没有变;
2. 只改变了标示需要重定位的那四个32位位置发生了变化。
0000000000001125 <main>:
1125: 55 push %rbp
1126: 48 89 e5 mov %rsp,%rbp
1129: b9 02 00 00 00 mov $0x2,%ecx
112e: 48 8d 15 fb 2e 00 00 lea 0x2efb(%rip),%rdx
1135: 48 8d 35 dc 2e 00 00 lea 0x2edc(%rip),%rsi
113c: 48 8d 3d cd 2e 00 00 lea 0x2ecd(%rip),%rdi
1143: e8 07 00 00 00 callq 114f <addvec>
1148: b8 00 00 00 00 mov $0x0,%eax
114d: 5d pop %rbp
114e: c3 retq
000000000000114f <addvec>:
二、链接
《深入了解计算机系统》第三版 第七章
1、静态链接
- 下图展示了一个简单程序从C源文件到可执行目标文件的执行过程,此处执行的是静态链接:
1.1、目标文件
- 可重定位目标文件
- 可执行目标文件(完全链接的)
- 共享目标文件:静态库,可重定位目标文件的归档文件。
1.2、可重定位目标文件
1.2.1、 可执行目标文件的格式如下:
Section |
Description |
ELF头 |
文件类型说明,机器相关内容说明、节头部表偏移和大小说明 |
.text |
代码段 |
.rodata |
只读数据段 |
.data |
已初始化的全局变量和静态变量 |
.bss |
未初始化的静态变量;被初始化为0的全局变量和静态变量 |
.symtab |
符号表,存放程序中定义和引用的函数和全局变量的信息 |
.rel.text |
.text节中位置的列表,链接时需要修改这些位置 |
.rel.data |
被模块引用或定义的所有全局变量的重定位信息 |
.debug |
一个调试符号表,包含程序中定义的局部变量和类型定义,程序中引用和定义的全局变量以及原始的C源文件 |
.line |
原始C源程序中的行号和.text节中机器指令间的映射。 |
.strtab |
一个字符串表,包含.symtab和.debug中所有符号及节头部中的节名字 |
节头部表 |
描述目标文件的节 |
1.2.2、可重定位目标文件各部分之间的关系
- 各部分说明:
- ELF头:是可重定位目标文件入口,定义可重定位目标文件的读取方式,以及节头部表的位置和大小。通过ELF头可以读到节头部表。
- 节头部表:包含各节的位置和大小,通过它可以定位到各节;
- symtab节:符号在strtab中的位置,可以读取各符号的名字;符号类型、符号的位置(符号所在节及节内偏移)、符号所占内存大小。这样就可以访问各个符号了。
- 重定位信息(.rel.text节和.rel.data节):需要重定位的位置(所在text节或data节的位置),知道哪里需要重定位;其所引用的符号在symtab中的位置,通过symtab就可以找到引用符号了。
- 代码段、只读数据段、初始化数据段及bbs段:这些是程序执行真正需要的东西,跟C源文件内容是对应的;在重定位过程中会被修改。ELF文件中其他节的内容其实都是为了访问和维护这些内容。
- debug段和line段:是调试需要的信息,要通过-g选项才能得到;此处咱不说明
- strtab段:符号的字符串表,通过symtab和debug段来访问。
- 下图是简单的访问关系图