基础知识

编译程序的过程

预处理:gcc -E -o hello.cpp hello.c -m32 (源代码)

  • 将所有的#define删除,并且展开所有的宏定义
  • 处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
  • 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置
  • 删除所有注释 “//”和”/* */”.
  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们

编译:gcc -x cpp-output -S -o hello.s hello.cpp -m32 (汇编代码)

  • 编译过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码

汇编:gcc -x assembler -c hello.s -o hello.o -m32(目标代码)

  • 汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令

链接:gcc -o hello hello.o -m32 (可执行文件)

  • 通过调用链接器ld来链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件

静态链接和动态链接

  • 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。
  • 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去

ELF格式

GOT表

  Global Offset Table,在位置无关代码中,一般不能包含绝对虚拟地址(如共享库)。当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是在运行阶段,符号的地址才会最终确定。因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,GOT表中每项保存程序中引用其它符号的绝对地址。这样,程序就可以通过引用GOT表来获得某个符号的地址。
  在x86结构中,GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。对于符号的动态解析过程,我们只需要了解的就是第二项和第三项,即GOT[1]和GOT[2]:GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址(加载的共享库会形成一个链表);GOT[2]保存的是一个函数的地址,定义如下:GOT[2] = &_dl_runtime_resolve,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中

PLT表

  Procedure Linkage Table,过程链接表(PLT)的作用就是将位置无关的函数调用转移到绝对地址。在编译链接时,链接器并不能控制执行从一个可执行文件或者共享文件中转移到另一个中(这时候函数的地址还不能确定),因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。
  在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。  

ELF文件符号动态解析的过程

  链接器在把所需要的共享库加载到内存后,并没有把共享库中的函数的地址写到GOT表中,而是延迟到函数的第一次调用时,才会对函数的地址进行定位。(红线为第一次函数调用的顺序,蓝线为后续函数调用的顺序)
下面以puts函数为例分析第一次函数调用的过程:
(1)进入puts@plt,执行指令
(2)跳到GOT表中相应位置,此时GOT表里保存的是pushl n的地址,实际上就是顺序执行下一步
(3)执行pushl n,n为puts函数地址在GOT表中的位置,向堆栈中压入这个偏移量的主要作用就是为了找到puts函数的符号名以及puts函数地址在GOT表项中所占的位置,以便在函数定位完成后将函数的实际地址写到这个位置。
(4)跳到PTL0的位置,这步将GOT[1],即共享库链表的地址压栈
(5)顺序执行jmp GOT[2],GOT[2]保存的是_dl_runtime_resolve函数的入口
(6)执行_dl_runtime_resolve函数,该函数会找到puts函数的实际加载地址,并把它写到GOT表中,返回时就会进入puts函数内执行。
  函数后续执行就会直接跳转的GOT表puts函数对应位置,该位置保存着puts函数的地址,直接进入puts函数。

查看hello文件

静态链接的hello文件:

动态链接的hello文件:

从图中可以看出动态链接使用了共享库,接下来查看共享库:

通过gdb调试,我们知道printf实际上是调用了puts函数:

用IDA打开动态链接的hello文件

可以看到puts函数通过在PLT表0x080482F0的位置跳转到GOT表0x0804A00C的位置,查看GOT表验证:

用IDA打开静态链接的hello文件

可以看到已经把puts函数的内容导入到代码段了。

装载和启动过程分析

execve函数调用流程如下:

load_elf_binary代码

load_elf_binary(struct linux_binprm *bprm)
{
	……
    if(elf_interpreter) //使用动态链接
    {
        ...
        //装载ld的起点  #获得动态连接器的程序起点
        elf_entry = load_elf_interp(&loc->interp_elf_ex,
				    interpreter,
				    &interp_map_addr,
				    load_bias);
		//动态链接的入口地址
        ...
    }
    else //使用静态链接
    {
        ...
        elf_entry = loc->elf_ex.e_entry;
		//静态链接的入口地址
        ...
    }
    ...
    //用从ELF文件中获取到的信息设置当前进程的数据段、堆栈等
	current->mm->end_code = end_code;
	current->mm->start_code = start_code;
	current->mm->start_data = start_data;
	current->mm->end_data = end_data;
	current->mm->start_stack = bprm->p;
	……
	//用上面初始化好的elf_entry重新覆盖成新的入口地址
    start_thread(regs,elf_entry,bprm->p);
}

上述代码可以看出:静态链接和动态链接的差别:加载之后的入口点不同,前者是直接跳转至程序入口点,后者是先跳转值链接器。

start_thread代码

start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)                                                          
set_user_gs(regs, 0);                                     
	regs->fs                = 0;                             
	regs->ds                = __USER_DS;                       
	regs->es                = __USER_DS;                       
	regs->ss                = __USER_DS;                      
	regs->cs                = __USER_CS;                       
	regs->ip                = new_ip;                          
	regs->sp                = new_sp;                         
	regs->flags             = X86_EFLAGS_IF;       
   /*                                           
* force it to the iret return path by making it look as if
* some work pending.                                      
set_thread_flag(TIF_NOTIFY_RESUME);                        
}           

设置新的eip和esp,即加载可执行程序启动的地方,我们打印new_ip的值:

查看hello程序的入口地址:

可以验证两者的地址是一样的,说明静态链接的hello程序从这里开始运行。

参考博客:ELF文件的加载和动态链接过程