linux里一般我们会直接用:
gcc 工程文件 -o 输出文件
来直接生成可执行文件,这个过程中gcc自己完成了预处理,编译,汇编的工作,并调用了连接器ld进行汇编。那能否尝试把这个过程分步完成呢?
进行尝试:
建立项目:
创建一个main.c,代码如下:
#include<stdio.h>
void main(){
printf("hello world\n");
}
gcc预处理:
预处理会处理掉代码中所有“#”开头的命令,如include,define等等。
命令行输入:
预处理生成.i文件
打开.i文件,发现#命令消失,取而代之的是数百行文字。
gcc编译:
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。
命令行输入:
编译生成.s文件:
打开.s文件,发现我们的代码已成为汇编模式:
gcc汇编:
汇编就是将汇编代码翻译成符合一定格式的机器代码,在Linux系统上一般表现为ELF目标文件(OBJ文件)。
命令行输入:
汇编生成.o文件:
.o文件已经是机器码,我们无法识别。
ld链接:
链接可以把.o文件和系统库的.o文件、.so库文件链接起来,最终生成可以在特定平台运行的可执行文件。
这一步比较复杂,因为一般不知道我们的文件用到了系统哪些库,所以先用编译器gcc直接生成可执行文件out,用ldd命令查看其用到的系统库:
(注:ldd只能识别隐式调用的动态链接库,不能识别显式链接的动态链接库,但ld命令只需要获取程序隐式调用的库文件即可)
把对应的库从系统目录下复制到当前目录(也可以ld命令直接写绝对路径或用-lc参数表示关联C库):
注意,不能复制符号链接,因为会因路径变更而失效,因此需要把符号链接对应的实际.so文件复制过来:
执行链接命令,其实对于这个简单的打印hello world程序,真正需要的只有libc-2.31.so这一个库,因为其中有printf函数的实现:另一个ld-linux-x86-64.so.2的功能下面会说。
报错无法找到_start,这是因为程序执行时真正入口并非main,而是_start,先不管,强行指定入口为main试试:
-e main表示指定程序的入口函数为main。
链接成功,尝试执行:
./out1
报错:没有这个文件或目录
这是因为由于手动链接,并未指定正确的程序解释器:
用readelf命令查看生成的out1文件读取的解释器:
发现出现需要的解释器及路径 /lib/ld64.so.1
我们进入这个路径,发现并没有ld64.so.1 ,再用ldd命令查看out1的依赖库:
发现是需要/lib/ld64.so.1这个库的,而上面查看的out的对应库为/lib64/ld-linux-x86-64.so.2,这其实就是两个文件要加载的解释器,两个解释器是同一个库文件,但不知为何手动链接生成的out1并非直接访问该库,而是想要使用/lib/ld64.so.1软链接。
解决办法:
方法一:
因此,把/lib64/ld-linux-x86-64.so.2这个文件建立一个软链接/lib/ld64.so.1:
执行后就有了/lib/ld64.so.1这个文件。
方法二:
在ld命令中直接指定解释器:
解决完毕后可以成功生成out1。
再执行out1:
发现出现段错误。
段错误解决
如果用gdb调试就会发现,报段错误是在main函数返回时,返回到了一个奇怪的地址0x1。这个地址是不合法的,因此出现段错误。
为什么会发生这个?
首先要了解,上文也说过,linux程序的入口并非是main函数,而是C库的_start函数,_start函数再调用__libc_start_main 函数再调用我们的main函数,这里就不详细叙述这个过程,只要简单知道,一般我们的链接器把main.o和C库链接起来时,生成的执行程序,会先进入名为_start的函数中,经过一系列操作再调用main函数,我们自己写的的程序功能结束后,main函数再return回_start中,_start函数正式终结整个进程。
那么出错的原因就简单了,我们是手动链接,gcc自动链接会链接_start并指定其为入口函数,但我们生成的out1中入口函数是main,main函数执行return后返回不到_start,因此在栈上取得了一个奇怪的地址0x1。这个地址应该是linux为保护程序设定的,地址无效并报段错误,以防跳到更加重要的位置引起重大错误。
那么该如何解决呢?可以思考,为什么_start函数的返回不会出现段错误,它返回到了哪里呢?其实程序执行不可能一直无限返回,必定有一个终点,这个终点就是_start,因为_start并没有调用return,而是使用了exit函数直接终结进程。
用 man 命令查看exit函数:
man exit:
发现其确实没有返回,说明是进程的终点。
因此改正段错误有两种方法,
1,要么_start函数,
2,让main使用exit。
对于方法一,可以先找到_start函数所在库文件,然后正常用ld命令该文件。这样生成程序基本完全和gcc直接编译链接产生的out一致。
但这里介绍一种取巧办法,即我们自己定义_start函数。
写一个start.c:
#include <stdlib.h>
void _start(){
main(); //调用main
exit(0); //终止进程
}
编译:
然后把这个.o文件链入我们的程序:
成功执行,只不过这个_start函数是我们伪造的。
对于方法二,修改main.c:
#include<stdio.h> //printf头文件
#include <stdlib.h> //exit头文件
void main(){
printf("hello world\n");
exit(0);
}
由于exit的实现也在libc-2.31.so中,因此不需额外链接库文件。
编译链接并执行:
成功执行。