图片选自:进程中的地址是从何而来
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 -E helloworld.c -o helloworld.i //将源代码做预处理(这里必须加-o,否则结果会输出到屏幕)
- gcc -S helloworld.i //将预处理后的代码进行汇编,生成汇编代码
- gcc -c helloworld.s //将汇编代码编译成目标文件,即二进制代码
- gcc helloworld.o -o helloworld //将多个目标文件链接成一个可执行文件或共享库文件
点击(此处)折叠或打开
- 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 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
为什么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 变量,尤其是作为全局变量。
本节参考:编译 链接和加载 ldconfig , ldd 与 LD