在Linux下,一个程序从源代码到执行,经过了以下几个过程:预处理(Pre-Processing)、编译(Compiling)、汇编(Assembling)、链接(Linking)、加载(Loading)、执行(Executing)。而广义上,我们经常将预处理、编译、汇编、链接统称为编译,把加载和执行统称为执行。
【进程的内存布局二】Linux 编译 执行-LMLPHP
图片选自:进程中的地址是从何而来
1 编译
  在Linux下,我们使用GNU编译工具gcc编译源代码,而gcc编译过程中所用的汇编器(as)和链接器(ld)是GNU Binutils提供的,使用gcc前一定要装GNU Binutils。在gcc编译源代码的过程中会依次调用:预处理器(cpp)做预处理(Pre-Processing),编译器(cc1)做编译(Compiling)、汇编器(as)做汇编(Assembling)、以及链接器(ld)做链接(Linking),对应的步骤为:
  以helloworld程序为例:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. int main()
  3. {
  4.     printf("Hello World!\n");
  5.     return 0;
  6. }
  通常,我们直接使用:gcc helloworld.c -o helloworld一次性向完成编译。

点击(此处)折叠或打开

  1. gcc helloworld.c -o helloworld
  其实,也可以分成4个步骤分步编译:(注意:可以按照键盘Esc键来记忆)

点击(此处)折叠或打开

  1. gcc -E helloworld.c -o helloworld.i    //将源代码做预处理(这里必须加-o,否则结果会输出到屏幕)
  2. gcc -S helloworld.i            //将预处理后的代码进行汇编,生成汇编代码
  3. gcc -c helloworld.s            //将汇编代码编译成目标文件,即二进制代码
  4. gcc helloworld.o -o helloworld        //将多个目标文件链接成一个可执行文件或共享库文件
  相应的,gcc工具会依次调用:

点击(此处)折叠或打开

  1. cpp helloworld.c -o helloworld.i            //预处理器(cpp)做预处理(Pre-Processing)
  2. /usr/lib/gcc/i686-linux-gnu/4.6/cc1 helloworld.i    //编译器(cc1)做编译(Compiling)
  3. as helloworld.s -o helloworld.o                //汇编器(as)做汇编(Assembling)
  4. 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选项打印出编译过程中调用的程序,确认一下以上过程对不对:

点击(此处)折叠或打开

  1. gcc -v helloworld.c -o helloworld 2>&1            //打印出编译过程中调用的程序到屏幕
  结果如下:

点击(此处)折叠或打开

  1. 使用内建 specs。
  2. COLLECT_GCC=gcc
  3. COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-linux-gnu/4.6/lto-wrapper
  4. 目标:i686-linux-gnu
  5. 配置为:../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
  6. 线程模型:posix
  7. gcc 版本 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
  8. COLLECT_GCC_OPTIONS='-v' '-o' 'helloworld' '-mtune=generic' '-march=i686'
  9.  /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
  10. GNU C (Ubuntu/Linaro 4.6.3-1ubuntu5) 版本 4.6.3 (i686-linux-gnu)
  11.     由 GNU C 版本 4.6.3 编译, GMP 版本 5.0.2,MPFR 版本 3.1.0-p3,MPC 版本 0.9
  12. GGC 准则:--param ggc-min-expand=98 --param ggc-min-heapsize=128150
  13. 忽略不存在的目录“/usr/local/include/i386-linux-gnu”
  14. 忽略不存在的目录“/usr/lib/gcc/i686-linux-gnu/4.6/../../../../i686-linux-gnu/include”
  15. #include "..." 搜索从这里开始:
  16. #include <...> 搜索从这里开始:
  17.  /usr/lib/gcc/i686-linux-gnu/4.6/include
  18.  /usr/local/include
  19.  /usr/lib/gcc/i686-linux-gnu/4.6/include-fixed
  20.  /usr/include/i386-linux-gnu
  21.  /usr/include
  22. 搜索列表结束。
  23. GNU C (Ubuntu/Linaro 4.6.3-1ubuntu5) 版本 4.6.3 (i686-linux-gnu)
  24.     由 GNU C 版本 4.6.3 编译, GMP 版本 5.0.2,MPFR 版本 3.1.0-p3,MPC 版本 0.9
  25. GGC 准则:--param ggc-min-expand=98 --param ggc-min-heapsize=128150
  26. Compiler executable checksum: 09c248eab598b9e2acb117da4cdbd785
  27. COLLECT_GCC_OPTIONS='-v' '-o' 'helloworld' '-mtune=generic' '-march=i686'
  28.  as --32 -o /tmp/ccpNZ3fp.o /tmp/ccuZjKnm.s
  29. 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/
  30. 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/
  31. COLLECT_GCC_OPTIONS='-v' '-o' 'helloworld' '-mtune=generic' '-march=i686'
  32.  /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 变量,尤其是作为全局变量。

本节参考:编译 链接和加载 ldconfig , ldd 与 LD

09-21 11:11