一、编译链接过程
#include<stdio.h> int main() { printf("Hello World!"); return 0; }
1.预处理,处理代码中的宏定义和 include
文件,并做语法检查.
gcc -E -o hello.cpp hello.c -32
2.编译成会变代码
gcc -x cpp-output -S -o hello.s hello.cpp -m32
3.汇编成目标代码
gcc -x assembler -c hello.s -o hello.o -m32
4.链接成可执行文件
gcc -o hello hello.o -m32
5.执行程序
./hello
6.选择静态编译
gcc -o hello.static hello.o -m32 -static
二、ELF文件格式
ELF
格式:可执行和可链接格式 (Executable and Linkable Format
) 是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式。
可重定位文件,如:.o 文件,包含代码和数据,可以被链接成可执行文件或共享目标文件,静态链接库属于这一类。 可执行文件,如:/bin/bash 文件,包含可直接执行的程序,没有扩展名。 共享目标文件,如:.so 文件,包含代码和数据,可以跟其他可重定位文件和共享目标文件链接产生新的目标文件,也可以跟可执行文件结合作为进程映像的一部分
ELF
文件包括 ELF header
和文件数据。其中文件数据包括:Program header table, 程序头:描述段信息 .text, 代码段:保存编译后得到的指令数据 .data, 数据段:保存已经初始化的全局静态变量和局部静态变量 Section header table, 节头表:链接与重定位需要的数据。
三、静态链接和动态链接
对于32位x86的机器来讲,进程地址空间共有4G,最上面1G供内核,下面3G供用户态使用。默认进程是从0x8048000开始加载,首先是ELF文件头部,再把代码和数据加载到进程的地址空间,ELF Header中的Entry point address即是可执行文件加载到内存开始执行的第一行代码。一般静态链接会将所有代码放在一个代码段,而动态链接的进程会有多个代码段。
可执行程序的执行环境
-
命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
-
$ ls -l /usr/bin 列出/usr/bin下的目录信息
-
Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身
-
例如,int main(int argc, char *argv[])
-
又如, int main(int argc, char *argv[], char *envp[])
-
Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
-
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
-
库函数exec*都是execve的封装例程
命令行参数和环境串都放在用户态堆栈中
命令行参数和环境变量的保存和传递是当我们创建一个子进程时,(fork是复制父进程),然后调用exece系统调用,它把要加载的可执行程序把原来的进程环境给覆盖掉了,覆盖了以后它的用户态堆栈也被清空。这时命令行参数和环境变量会被压栈。
shell程序 -> execve -> sys_execve 然后在初始化新进程堆栈时拷贝进去。
int execve(const char * filename , char * const argv[] , char * const envp[])
创建了一个新的用户态堆栈的时候,实际上是把命令行参数(argv[])的内容和环境变量(envp[])的内容通过指针的方式传递给系统调用内核处理函数的,然后内核处理函数再建一个可执行程序新的用户态堆栈的时候,会把这些拷贝到用户态堆栈,初始化新的可执行程序执行的上下文环境。先函数调用参数传递,再系统调用参数传递。
四、装载
五、总结
Linux
系统通过用户态 execve
函数调用内核态 sys_execve
系统调用,sys_execve()服务例程修改当前进程的执行上下文,将新的程序代码和数据替换到新的进程中,打开可执行文件,载入依赖的库文件,申请新的内存空间,最后执行 start_thread
函数设置 new_ip
和 new_sp
,完成新进程的代码和数据替换,然后返回并执行新的进程代码。