可执行文件的生成与加载

时间:2021-12-01 12:48:45

可执行文件的生成与加载
(1)预处理,得到预处理文件hello.i,它还是一个可读的文本文件 ,但不包含任何宏定义
$gcc –E hello.c –o hello.i
$cpp hello.c > hello.i
PS:gcc命令实际上是具体程序(如ccp、cc1、as等)的包装命令,用户通过gcc命令来使用具体的预处理程序ccp、编译程序ccl和汇编程序as等
处理源文件中以“#”开头的预编译指令,包括:
  1. 删除“#define”并展开所定义的宏
  2. 处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等
  3. 插入头文件到“#include”处,可以递归方式进行处理
  4. 删除所有的注释“//”和“/* */”
  5. 添加行号和文件名标识,以便编译时编译器产生调试用的行号信息
  6. 保留所有#pragma编译指令(编译器需要用)
(2)编译,就是将预处理后得到的预处理文件进行词法分析、语法分析、语义分析、优化后,生成汇编代码文件
用来进行编译处理的程序称为编译程序(编译器,Compiler)
$gcc –S hello.i –o hello.s
$gcc –S hello.c –o hello.s
$ccl hello.i -o hello.s
$/user/lib/gcc/i486-linux-gnu/4.1/cc1 hello.c
得到的结果是可重定位目标文件hello.o(VC下是.obj文件),其中包含的是不可读的二进制代码,必须用相应的工具软件来查看其内容
(3)汇编,汇编程序(汇编器)用来将由汇编指令构成的汇编语言源程序转换为机器指令序列(机器语言程序)
汇编指令和机器指令一一对应,前者是后者的符号表示,它们都属于机器级指令,所构成的程序称为机器级代码
$gcc –c hello.s –o hello.o
$gcc –c hello.c –o hello.o 
$as hello.s -o hello.o    //as是一个汇编程序
(4)链接过程将多个可重定位目标文件合并以生成可执行目标文件。其包含的代码和数据可以直接被复制到内存执行。
早期由手动完成,现在由链接程序——链接器完成。
$gcc –static –o myproc main.o test.o
$ld –static –o myproc main.o test.o
//static 表示静态链接
//如果不指定-o选项,则可执行文件名为“a.out”
//Windows中为.exe文件
高级编程语言中,子程序(函数)起始地址和变量起始地址是符号定义(definition),调用子程序(函数或过程)和使用变量即是符号的引用(reference)。一个模块定义的符号可以被另一个模块引用 。 最终须链接(即合并),合并时须在符号引用处填入定义处的地址。
链接本质:合并相同的节
可执行文件的生成与加载
链接后,程序在磁盘中,不知道程序会装到内存何处执行,因此实际上是合并到虚拟地址空间中。程序头表描述如何映射。
每个.o文件代码和数据地址都是从0开始。链接之后读写数据段和只读代码段地址都高于0x08048000
 
链接操作的步骤:
Step 1. 符号解析(Symbol resolution)
–    程序中有定义和引用的符号 (包括变量和函数等)
–    编译器将定义的符号存放在一个符号表( symbol table)中
    –    .symtab 节记录符号表信息,是一个结构数组 
    –    每个表项包含符号名、长度和位置等信息     
–   链接器将每个符号的引用都与一个确定的符号定义建立关联
 
每个可重定位目标模块都有一个符号表,它包含了在模块中定义和引用的符号。有三种链接器符号:
  • Global symbols(模块内部定义的全局符号):由模块m定义并能被其他模块引用的符号。
           例如,非static C函数和非static的C全局变量(指不带static的全局变量)
  • External symbols(外部定义的全局符号):由其他模块定义并被模块引用的全局符号
  • Local symbols(本模块的局部符号):仅由模块m定义和引用的本地符号。
           例如,在模块m中定义的带static的函数和全局变量
           不是指程序中的局部变量(分配在栈中的临时性变量),链接器不关心这种局部变量。他们运行时动态分配,不记录于符号表。
ELF文件中的符号表(.symtab节)中每个表项(16B)的结构如下:
typedef  struct {
    Elf32_Word st_name;  /*符号对应字符串在.strtab节中的偏移量*/
    Elf32_Addr st_value;    /*在对应节(函数名在text节中,变量名在data节或bss节中)中的偏移量,或虚拟地址*/
    Elf32_Word st_size;     /*符号对应目标字节数,即函数大小或变量长度*/
    unsigned char st_info; /*低4位指出符号的类型(Type,包括数据、函数、源文件、节、未知),高4位指定绑定属性(Bind,包括全局符号、局部符号、弱符号) */
    unsigned char st_other;/*在可重定位文件中指定可见性,定义当符号成为可执行文件或共享目标库文件的一部分后访问该符号的方式 */
    Elf32_Word st_shndx;    /*指出符号所在节在节头表中的索引,有些符号属于伪节(节头表无表项,无法表示索引值):ABS表示该符号不会由于重定位发生值的变化,不该被重定位;UND表示未定义;COM表示还未被分配位置的未初始化数据(.bss),此时,st_value表示对齐要求,st_size给出最小长度  */
例如main.c
int buf[2]={1,2};
int main(){
    swap(); 
    return 0;
}
和swap.c
extern int buf[]; 
int *bufp0 = &buf[0]; 
static int *bufp1; 
void swap(){
    int temp;
    bufp1 = &buf[1]; 
    temp = *bufp0;
    *bufp0 = *bufp1;
    *bufp1 = temp;
}
查看main.o中的符号表中最后三个条目(共10个)
buf是main.o中第3节(.data)偏移为0的符号,是全局变量,占8B;
main是第1节(.text)偏移为0的符号,是全局函数,占33B;
swap是未定义的符号,不知道类型和大小,全局的(在其他模块定义)
可执行文件的生成与加载
查看swap.o中的符号表中最后4个条目(共11个)
bufp1是未分配地址且未初始化的本地变量(ndx=COM), 按4B对齐且占4B
可执行文件的生成与加载
 
符号定义的实质就是指被分配了存储空间。为函数名即指其代码所在区;为变量名即指其所占的静态数据区。所有定义符号的值就是其目标所在的首地址。
符号解析也称符号绑定,目的是将每个模块中引用的符号与某个目标模块中的定义符号建立关联。
每个定义符号在代码段或数据段中都被分配了存储空间,将引用符号与定义符号建立关联后,就可在重定位时将引用符号的地址重定位为相关联的定义符号的地址。
本地符号在本模块内定义并引用,因此,其解析较简单,只要与本模块内唯一的定义符号关联即可。
全局符号(外部定义的、内部定义的)的解析涉及多个模块,故较复杂。
例如:int *xp=&x引用符号x对符号xp进行了定义。
 
函数名和已初始化的全局变量名是强符号;未初始化的全局变量名是弱符号。
符号解析时只能有一个确定的定义(即每个符号仅占一处存储空间)
Rule 1: 强符号只能被定义一次,否则链接错误
Rule 2: 若一个符号被定义为一次强符号和多次弱符号,则按强定义为准。对弱符号的引用被解析为其强定义符号。
Rule 3: 若有多个弱符号定义,则任选其中一个。使用命令 gcc –fno-common链接时,会告诉链接器遇到多个弱定义的全局符号时输出一条警告信息。
 
Step 2. 重定位
–    合并相关.o文件。将多个代码段与数据段分别合并为一个单独的代码段和数据段 
–    对定义符号进行重定位(确定地址),计算每个定义的符号在虚拟地址空间中的绝对地址 
例如,为函数确定首地址,进而确定每条指令的地址,为变量确定首地址 
完成这一步后,每条指令和每个全局或局部变量都可确定地址   
–    对引用符号进行重定位(确定地址),将可执行文件中符号引用处的地址修改为重定位后的地址信息
需要用到在.rel_data和.rel_text节中保存的重定位信息
 
汇编器遇到引用时,生成一个重定位条目:反映符号引用的位置、绑定的定义符号名、重定位类型
数据引用的重定位条目在.rel_data节中、指令中引用的重定位条目在.rel_text节中 
用readelf命令可显示main.o中的重定位条目(表项)
$ readelf -r main.o
ELF中重定位条目格式如下:
typedef  struct {
    int  offset;          /*节内偏移*/
    int  symbol:24,       /*所绑定符号*/
         type: 8;         /*重定位类型*/
} Elf32_Rel;
IA-32有两种最基本的重定位类型:
  • R_386_32: 绝对地址 
  • R_386_PC32: PC相对地址
(1)buf的定义在.data节中偏移为0处开始,占8B。
Disassembly of section .data:
00000000 <buf>:
0:   01 00 00 00 02 00 00 00
(2)main的定义在.text节中偏移为0处开始,占0x12B(18B)
Disassembly of section .text: 
00000000 <main>:
0:     55                push %ebp
1:     89 e5             mov %esp,%ebp
3:     83 e4 f0          and $0xfffffff0,%esp  //用与指令调整栈顶为16的倍数      
6:     e8 fc ff ff ff    call 7 <main+0x7>     //fffffffc=-4
                         7: R_386_PC32 swap    //汇编器同时会生成一个重定位表项。
b:     b8 00 00 00 00    move $0x0,%eax
10:    c9                leave
11:    c3                ret
在重定位节rel_text节中有重定位条目:r_offset=0x7,r_sym=10,r_type=R_386_PC32,
被OBJDUMP工具以“7: R_386_PC32 swap”的可重定位信息显示在需重定位的call指令的下一行。
即告诉链接器对.text节中偏移量为7的位置进行重定位,重定位到main.o(自身)符号表的第十个符号swap,重定位方式为PC相对地址
swap是main.o的符号表中第10项,是未定义符号,类型和大小未知,并是全局符号,故在其他模块中定义。
假定可执行文件中main函数对应机器从0x8048380开始(0x8048000-0x8048380中间还有一些系统代码),长0x12B
假定swap紧跟main后,机器代码首地址按4字节边界对齐,起始地址为0x8048394
根据call指令的机器代码“e8 fc ff ff ff”可知,需重定位的4字节地址的初始值(init)为0xfffffffc (小端方式下是-4)。
汇编器用-4作为偏移量,得到call指令的下条指令开始处的地址PC=0x8048380+0x07-init=0x804838b,此处相对于需重定位的地址偏移为4个字节。
重定位值=转移目标地址-PC=ADDR(r_sym) – ( ( ADDR(.text) + r_offset ) – init )
                                         引用目标处      call指令下一条指令地址,即PC
本例中:ADDR(r_sym) = 0x8048394, ADDR(.text) = 0x8048380, r_offset=0x7,而init=-4 ,最终求得重定位值0x9。
因此,call指令实际执行了:
R[eip]=0x804838b
R[esp]← R[esp]-4    
M[R[esp]] ←R[eip]
R[eip] ←R[eip]+0x9
(3)swap.o中.data节内容:
Disassembly of section .data
00000000 <bufp0>:
0:R_386_32  buf
在重定位节.rel.data中的重定位条目为:r_offset=0x0,r_sym=9, r_type=R_386_32,
OBJDUMP工具解释后显示为0:R_386_32 buf
即告诉链接器对.data节偏移量为0的位置进行重定位,重定位到swap.o(自身)符号表的第9个符号buf,重定位方式为绝对地址
buf是swap.o的符号表中第9项,是未定义符号,类型和大小未知,并是全局符号,故在其他模块中定义。
重定位值初始值0,重定位后为初始值加所引用符号地址
假定buf在运行时的存储地址ADDR(buf)=0x8049620,则重定位后,buf和bufp0同属于.data节,故在可执行文件中它们被合并。 bufp0紧接在buf后,故地址为0x8049620+8=0x8049628 。
因是R_386_32方式,故bufp0内容为buf的绝对地址0x8049620,即“20 96 04 08”
因此,合并后的可执行目标文件中.data节的内容:
Disassembly of section .data:
08049620 <buf>: 
    8049620:    01 00 00 00 02 00 00 00
08049628 <bufp0>:
    8049628:    20 96 04 08
(5)bufp1的地址就是链接合并后.bss节的首地址,假定为0x8049700
 
 
可执行文件的加载:
(1)UNIX/Linux系统中,在shell命令行提示符后输入命令:$./hello[enter]
(2)shell命令行解释器构造argv和envp
可执行文件的生成与加载
(3)调用fork()函数,创建一个子进程,与父进程shell完全相同(只读/共享),包括只读代码段、可读写数据段、堆以及用户栈等。
(4)可通过调用execve()系统调用函数来启动加载器,在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间。
execve()函数的功能是在当前进程上下文中加载并运行一个新程序,用法如下:
int execve(char *filename, char *argv[], *envp[]);
filename是加载并运行的可执行文件名(如./hello),
参数列表argv和环境变量列表envp都用一个以null结尾的指针数组表示,每个数组元素都指向一个用字符串表示的参数(通常argv[0]指向可执行文件目标名,后面依次是指向命令各个参数的指针)或者环境变量串(指向的每个字符串都是一个形如"NAME= VALUE"的名-值对)。
若错误(如找不到指定文件filename) ,则返回-1,并将控制权交给调用程序; 若函数执行成功,则不返回 ,最终将控制权传递到可执行目标中的主函数main。
加载器(loader)根据可执行文件的程序(段)头表中的信息,将可执行文件的代码和数据从磁盘“拷贝”到存储器中(实际上不会真正拷贝,仅建立一种映射)
(5)加载后,将PC(EIP)设定指向Entry point (即符号_start处),最终执行main函数,hello程序开始在一个进程的上下文中运行。
程序入口地址并不是0x8048000,因为前面是ELF头和程序头表,是在.init节。
主函数main()的原型形式如下:
int main(int argc, char **argv, char **envp);
int main(int argc, char *argv[], char *envp[]);
argc指定参数个数,参数列表中第一个总是命令名(可执行文件名)
例如:命令行为
ld -o test main.o test.o
argc=6(最后有一个空)
当IA-32/Linux系统开始执行main()函数时, 在虚拟地址空间的用户栈中具有如图所示的组织结构:
可执行文件的生成与加载
用户栈的栈底是一系列环境变量串, 然后是命令行参数串, 每个串以null结尾, 连续存放在栈中,每个串i由相应的envp[i]和argv[i中的指针指示。
在命令行参数串后面是指针数组envp的数组元素, 全局变量environ指向这些指针中的第一个指针envp[0]
然后是指针数组argv的数组元素。
在栈的顶部是main()函数的三个参数: envp、argv和argc。
在这三个参数所在单元的后面将生成main()函数的栈帧。
(6)总结
_start:  __libc_init_first→_init→atexit(设置出口位置)→main→_exit(执行出口处的结束代码)