在Linux下,一个程序从源代码到执行,经过了以下几个过程:预处理(Pre-Processing)、编译(Compiling)、汇编(Assembling)、链接(Linking)、加载(Loading)、执行(Executing)。而广义上,我们经常将预处理、编译、汇编、链接统称为编译,把加载和执行统称为执行。
图片选自:进程中的地址是从何而来
1 编译
在Linux下,我们使用GNU编译工具gcc编译源代码,而gcc编译过程中所用的汇编器(as)和链接器(ld)是GNU Binutils提供的,使用gcc前一定要装GNU Binutils。在gcc编译源代码的过程中会依次调用:预处理器(cpp)做预处理(Pre-Processing),编译器(cc1)做编译(Compiling)、汇编器(as)做汇编(Assembling)、以及链接器(ld)做链接(Linking),对应的步骤为:
以helloworld程序为例:
-
#include <stdio.h>
-
int main()
-
{
-
printf("Hello World!\n");
-
return 0;
-
}
通常,我们直接使用:gcc helloworld.c -o helloworld一次性向完成编译。
-
gcc helloworld.c -o helloworld
其实,也可以分成4个步骤分步编译:(注意:可以按照键盘Esc键来记忆)
-
gcc -E helloworld.c -o helloworld.i //将源代码做预处理(这里必须加-o,否则结果会输出到屏幕)
-
gcc -S helloworld.i //将预处理后的代码进行汇编,生成汇编代码
-
gcc -c helloworld.s //将汇编代码编译成目标文件,即二进制代码
-
gcc helloworld.o -o helloworld //将多个目标文件链接成一个可执行文件或共享库文件
相应的,gcc工具会依次调用:
-
cpp helloworld.c -o helloworld.i //预处理器(cpp)做预处理(Pre-Processing)
-
/usr/lib/gcc/i686-linux-gnu/4.6/cc1 helloworld.i //编译器(cc1)做编译(Compiling)
-
as helloworld.s -o helloworld.o //汇编器(as)做汇编(Assembling)
-
ld -dynamic-linker /lib/ld-linux.so.2 helloworld.o -o helloworld /usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu/crt1.o /usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu/crti.o /usr/lib/gcc/i686-linux-gnu/4.6/crtbegin.o -lc /usr/lib/gcc/i686-linux-gnu/4.6/crtend.o /usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu/crtn.o //链接器(ld)做链接(Linking)
现在,我们通过gcc -v选项打印出编译过程中调用的程序,确认一下以上过程对不对:
-
gcc -v helloworld.c -o helloworld 2>&1 //打印出编译过程中调用的程序到屏幕
结果如下:
-
使用内建 specs。
-
COLLECT_GCC=gcc
-
COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-linux-gnu/4.6/lto-wrapper
-
目标:i686-linux-gnu
-
配置为:../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.6.3-1ubuntu5' --with-bugurl=file:///usr/share/doc/gcc-4.6/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.6 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.6 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --enable-objc-gc --enable-targets=all --disable-werror --with-arch-32=i686 --with-tune=generic --enable-checking=release --build=i686-linux-gnu --host=i686-linux-gnu --target=i686-linux-gnu
-
线程模型:posix
-
gcc 版本 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
-
COLLECT_GCC_OPTIONS='-v' '-o' 'helloworld' '-mtune=generic' '-march=i686'
-
/usr/lib/gcc/i686-linux-gnu/4.6/cc1 -quiet -v -imultilib . -imultiarch i386-linux-gnu helloworld.c -quiet -dumpbase helloworld.c -mtune=generic -march=i686 -auxbase helloworld -version -fstack-protector -o /tmp/ccuZjKnm.s
-
GNU C (Ubuntu/Linaro 4.6.3-1ubuntu5) 版本 4.6.3 (i686-linux-gnu)
-
由 GNU C 版本 4.6.3 编译, GMP 版本 5.0.2,MPFR 版本 3.1.0-p3,MPC 版本 0.9
-
GGC 准则:--param ggc-min-expand=98 --param ggc-min-heapsize=128150
-
忽略不存在的目录“/usr/local/include/i386-linux-gnu”
-
忽略不存在的目录“/usr/lib/gcc/i686-linux-gnu/4.6/../../../../i686-linux-gnu/include”
-
#include "..." 搜索从这里开始:
-
#include <...> 搜索从这里开始:
-
/usr/lib/gcc/i686-linux-gnu/4.6/include
-
/usr/local/include
-
/usr/lib/gcc/i686-linux-gnu/4.6/include-fixed
-
/usr/include/i386-linux-gnu
-
/usr/include
-
搜索列表结束。
-
GNU C (Ubuntu/Linaro 4.6.3-1ubuntu5) 版本 4.6.3 (i686-linux-gnu)
-
由 GNU C 版本 4.6.3 编译, GMP 版本 5.0.2,MPFR 版本 3.1.0-p3,MPC 版本 0.9
-
GGC 准则:--param ggc-min-expand=98 --param ggc-min-heapsize=128150
-
Compiler executable checksum: 09c248eab598b9e2acb117da4cdbd785
-
COLLECT_GCC_OPTIONS='-v' '-o' 'helloworld' '-mtune=generic' '-march=i686'
-
as --32 -o /tmp/ccpNZ3fp.o /tmp/ccuZjKnm.s
-
COMPILER_PATH=/usr/lib/gcc/i686-linux-gnu/4.6/:/usr/lib/gcc/i686-linux-gnu/4.6/:/usr/lib/gcc/i686-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/4.6/:/usr/lib/gcc/i686-linux-gnu/
-
LIBRARY_PATH=/usr/lib/gcc/i686-linux-gnu/4.6/:/usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/4.6/../../../../lib/:/lib/i386-linux-gnu/:/lib/../lib/:/usr/lib/i386-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/i686-linux-gnu/4.6/../../../:/lib/:/usr/lib/
-
COLLECT_GCC_OPTIONS='-v' '-o' 'helloworld' '-mtune=generic' '-march=i686'
-
/usr/lib/gcc/i686-linux-gnu/4.6/collect2 --sysroot=/ --build-id --no-add-needed --as-needed --eh-frame-hdr -m elf_i386 --hash-style=gnu -dynamic-linker /lib/ld-linux.so.2 -z relro -o helloworld /usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu/crt1.o /usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu/crti.o /usr/lib/gcc/i686-linux-gnu/4.6/crtbegin.o -L/usr/lib/gcc/i686-linux-gnu/4.6 -L/usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu -L/usr/lib/gcc/i686-linux-gnu/4.6/../../../../lib -L/lib/i386-linux-gnu -L/lib/../lib -L/usr/lib/i386-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/i686-linux-gnu/4.6/../../.. /tmp/ccpNZ3fp.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-linux-gnu/4.6/crtend.o /usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu/crtn.o
很显然,可以看得出:先调用cc1,再调用as,最后调用collect2。那么你会问:为什么cpp没有被调用?为什么ld没有被调用?
为什么cpp没有被调用?
实验:我将系统中所有的cpp删除,使用gcc -E helloworld.c -o helloworld.i预处理,仍然能够产生helloworld.i,说明没人调用cpp。然后,我又readelf -s /usr/lib/gcc/i486-linux-gnu/4.4.3/cc1 | grep cpp,发现cc1中有很多含有cpp的函数,所以我怀疑在cc1中本身实现了cpp的预处理功能。
结论:使用gcc时,调用的cc1会做预处理(Pre-Processing),所以就没有再调用cpp。但是单独cpp helloworld.c -o helloworld.i 和 使用gcc -E helloworld.c -o helloworld.i 产生的helloworld.i无差别的,所以cpp单独使用也是ok的。
为什么ld没有被调用?
实验:我将系统中/usr/bin/ld删除后,使用gcc -v helloworld.c -o helloworld 2>&1编译,最后报错:collect2: 找不到‘ld’,说明ld确实被调用了。是collect2调用的嘛?尝试/usr/lib/gcc/i686-linux-gnu/4.6/collect2 --help,说明collect2会调用ld。
结论:使用gcc时,
collect2除了做一些辅助工作外,最终会调用ld做链接(Linking)。【collect2 ld 关系】
本节参考:
GCC
2 执行
在Linux下,系统通过调用execve()来执行程序,接着系统会为相应格式的文件查找合适的加载处理函数,而ELF格式目标文件的加载函数是load_elf_binary()。在load_elf_binary()中,系统会查找.interp段指定的动态加载器(/lib/ld-linux.so.2),并执行动态加载器(/lib/ld-linux.so.2),接着系统就会把控制权交给动态加载器(/lib/ld-linux.so.2),由动态加载器(/lib/ld-linux.so.2)寻找程序所需要的共享库(.so),并进行加载,然后进行符号查找和重定位。这个过程常常被称为“
动态链接过程”。以上过程完成后,动态加载器(/lib/ld-linux.so.2)所要做的事情就完成了,之后就会把控制权交给可执行程序的入口,开始执行程序。(注意:动态加载器(/lib/ld-linux.so.2)是共享库的加载器,不能直接在命令行下执行,在执行程序时会自动执行)
可以通过
objdump -s main | grep interp 或 readelf -l main | grep interpreter 查看ELF格式目标文件.interp段中,指定的动态加载器路径。
可以通过
objdump -x main | grep NEEDED 或 readelf -d main | grep NEEDED 查看ELF
格式目标文件.dynamic段中,依赖的共享库(.so)。也可以通过
ldd main 查看ELF
格式目标文件中,依赖的共享库(.so),包括动态加载器
(/lib/ld-linux.so.2)路径。
在Linux下,动态加载器(/lib/ld-linux.so.2)是如何寻找程序所依赖的共享库(.so)?
首先,动态加载器(/lib/ld-linux.so.2)在标准路经(/lib, /usr/lib) 中查找。
其次,如果所依赖的共享库在非标准路径下,动态加载器(/lib/ld-linux.so.2)是如何查找的呢?
目前,Linux通用的做法是将非标准路经加入 /etc/ld.so.conf,然后执行
ldconfig 生成 /etc/ld.so.cache。 动态加载器(/lib/ld-linux.so.2)加载共享库的时候,会从 /etc/ld.so.cache 中查找。
传统上,Linux的先辈 Unix 还有一个环境变量:LD_LIBRARY_PATH 来处理非标准路经的共享库。动态加载器(/lib/ld-linux.so.2)加载共享库的时候,也会查找这个变量所设置的路经。但是,有不少声音主张要避免使用 LD_LIBRARY_PATH 变量,尤其是作为全局变量。
本节参考:
编译 链接和加载
阅读(1036) | 评论(0) | 转发(0) |