1. gcc编译过程
gcc一些编译选项
a. 预处理(Pre-Processing):gcc -E hello.c -o hello.i
1)读取C/C++源程序,对其中的伪指令(以#开头的指令)进行处理。
- 将所有的“#define”删除,并且展开所有的宏定义。
- 处理所有的条件编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“endif”等,将那些不必要的代码过滤掉。
- 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。
2)删除所有的注释。
3)添加行号和文件名标识。
b. 编译(Compiling):gcc -S hello.i -o hello.s
将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。
c. 汇编(Assembling):gcc -c hello.s -o hello.o
将编译完的汇编代码文件翻译成机器指令,并生成可重定位目标程序的.o文件。可以使用file命令来查看hello.o的文件格式。
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
知道这是一个elf格式的文件,elf文件的格式如下图所示,贴了三张不同角度的图片,自行领会。
- Section:它的描述信息放在section header table。Sections是在ELF文件里头,用以装载内容数据的最小容器。
在ELF文件里面,每一个Sections 内都装载了性质属性都一样的内容。
1) .text section:装载了可执行代码;
2) .data section:装载了被初始化的数据;
3) .bss section:装载了未被初始化的数据,由于数据是不需要初始化的,所以.bss段在文件中只占一个Section Header而没有对应的Section,
程序加载时.bss段占多大内存空间在Section Header中描述;
4) 以 .rec 打头的 sections:装载了重定位条目,标识了需要进行重定位之处,重定位条目的每一项包含两个字段,addr和val,即
需要将addr的内容(也是一个地址)改成val,等价于$(addr) = val。重定位条目在链接时生成全局的val。
5) .symtab 或者 .dynsym section:装载了符号信息;
6) .strtab 或者 .dynstr section:装载了字符串信息;
一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的 section head table(SHT)决定。在SHT中,针对每一个section,都设置有一个条目,
用来描述对应的这个section,其内容主要包括该 section 的名称、类型、大小以及在整个ELF文件中的字节偏移位置等等。
- 符号表:每个可重定位目标模块 m 都有一个符号表,它包含了在 m 中定义和引用的符号。
1)Global symbols(模块内部定义的全局符号):由模块 m 定义并能被其他模块引用的符号。例如,非static C函数和非static C全局变量。
2)External symbols(外部定义的全局符号):由其他模块定义并被模块 m 引用的全局符号。
3)Local symbols(仅由模块 m 定义和引用的本地符号)。例如,在模块 m 中定义的带 static 的 C 函数和全局变量。
注:链接器的局部符号不是指程序中的局部变量(分配在栈中的临时性变量),链接器不关心这种局部变量。
可以通过readelf -s查看目标文件的符号表信息,指向readelf -s hello.o,输出如下结果:
Num: Value Size Type Bind Vis Ndx Name 310: 00000000 0 NOTYPE GLOBAL DEFAULT ABS _gp 734: 00000000 32 OBJECT GLOBAL DEFAULT 77 v 818: 00000000 496 FUNC GLOBAL DEFAULT 71 main 849: 00000000 4 OBJECT GLOBAL DEFAULT 78 phrase 955: 00000000 9 OBJECT GLOBAL DEFAULT 77 peppers 1020: 00000000 192 OBJECT GLOBAL DEFAULT 80 bins
- Num
= 符号序号
- Value
= 符号的地址,这里全都是0,是因为编译过程中不分配地址(分配了也没用),等到链接中进行重定位的时候才分配地址。
- Size
= 符号的大小
- Type
= 符号类型: Func
= Function, Object
, File
(source file name), Section
= memory section, Notype
= untyped absolute symbol or undefined
- Bind
= GLOBAL
绑定意味着该符号对外部文件的可见. LOCAL
绑定意味着该符号只在这个文件中可见. WEAK
有点像GLOBAL, 该符号可以被重载.
- Vis
= Symbols can be default, protected, hidden or internal.
- Ndx
= The section number the symbol is in. ABS means absolute: not adjusted to any section address's relocation
- Name
= symbol name
d. 链接(Linking):gcc hello.o -o hello
通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序(静态链接)。
主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,
使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
1)确定符号引用关系(符号解析):得出各个输入模块的代码段和数据段的大小
2)合并相关 .o 文件(重定位):链接器将所有相同类型的节合并为同一类型的新的聚合节。例如来自输入模块的.data节全部合并成一个节,
这个节成为输出可执行目标文件的.data节。
3)确定每个符号的地址(重定位):各个.o文件合并后,整个模块的逻辑地址从0开始,这时需要确定代码节和数据节中每个符号在最终的模块中的逻辑地址。
4)在指令中填入新的地址(重定位):链接器依赖重定位条目,修改每个地址为最终的逻辑地址。执行的时候需要通过页表寄存器进行寻址。
2. 程序的加载与执行
程序要运行是必然要把程序加载到内存中的。在过去的机器里都是把整个程序都加载进入物理内存中,现在一般都采用了虚拟存储机制,即每个进程都有完整的地址空间,
给人的感觉好像每个进程都能使用完成的内存。然后由一个内存管理器把虚拟地址映射到实际的物理内存地址。
运行可执行文件时,系统使用一个被称为加载器(loader)的程序,将可执行文件的代码和数据从磁盘加载到内存中,然后跳转到程序的第一条
指令(或者入口点entry point)开始执行。可执行程序的内存映像大概如下图:
代码段总是从地址0x08048000开始,数据段是在紧接着的一个4KB对齐的地址处,堆在数据段之后,往上增长。中间有一个共享库保留的内存段。
然后是用户栈,栈从最大的合法用户地址开始,向下增长。栈之上是系统保留的内存,用户进程不能访问(只能通过系统调用陷入内核态访问)。
程序从磁盘到内存可分为3步骤:
1)首先是创建虚拟地址到物理内存的映射(创建内核地址映射结构体),创建页目录和页表;
2)再就是加载代码段和数据段;
3)把可执行文件的入口地址写到CPU的PC寄存器中。