从helloworld回顾程序的编译过程之二

时间:2022-05-07 22:46:49

         为简单起见,本文中的例子将不使用printf之类的标准库函数,文中只使用自己定义的函数,下面的例子中主要是在linux环境下进行验证和调试的,如果没有linux开发环境也不要紧,本文已经将在linux下调试的结果全部都复制进来了,应该不影响阅读。
下面为本文中所使用的三个文件:
 //文件1:numOper.h

 int add(int iN1, int iN2);


 //文件2:numOper.c

 #include "numOper.h"
 int add(int iN1, int iN2)
 {
  return iN1 + iN2;
 }


 //文件3:hello.c

 //hello.c
 #include "numOper.h"

 #define NUM_1 10
 const int NUM_2 = 15;

 int main()
 {
  //will call add
  3+add(NUM_1, NUM_2);
  return 0;
 }


 一、预处理:
 
  1、对上述文件进行预编译,使用如下命令,需要注意的是包括预编译在内的编译过程都是以每个源文件为单位(即.c、.cpp等文件):
     使用如下两个命令将源文件hello.c和numOper.c进行预编译处理,
   # gcc -E hello.c -o hello.i
   # gcc -E numOper.c -o numOper.i
   
     执行上述两条命令之后,可以看到在目录下已经生成了numOper.i和hello.i两个预编译文件,打开这两个文件可以看到其内容为:
   hello.i文件的内容为:
    # 1 "hello.c"
    # 1 "<built-in>"
    # 1 "<command-line>"
    # 1 "hello.c"

    # 1 "numOper.h" 1


    int add(int iN1, int iN2);
    # 3 "hello.c" 2


    const int NUM_2 = 15;

    int main()
    {

     3+add(10, NUM_2);
     return 0;
    }
    
   numOper.i文件的内容为:
    # 1 "numOper.c"
    # 1 "<built-in>"
    # 1 "<command-line>"
    # 1 "numOper.c"

    # 1 "numOper.h" 1


    int add(int iN1, int iN2);
    # 3 "numOper.c" 2
    int add(int iN1, int iN2)
    {
     return iN1 + iN2;
    }

  可以看到在经过预编译处理之后的预编译文件中有这些变化:
   1)、每个编译单元(即源文件hello.c和numOper.c)都把其包含的头文件中的内容复制过来了;
   2)、从hello.i文件中可以看到源文件hello.c中定义的宏#define NUM_1 10被替换成了其所代表的数字10
   3)、从hello.i文件中可以看到源文件hello.c中的注释被删掉了。
   4)、从hello.i文件中可以看到源文件hello.c中定义的常量依然像正常变量一样存在,它们会一直存在着,
   到后面的编译阶段进行还会对它们进行类型检查,一直以来我们都提倡使用const代替#define宏,就是基于这方面的考虑,
   所有的宏在这一步就被处理掉了,而const常量则会存在,编译器会对const常量进行检查,如果使用错了就会有警告。
   
   
 二、编译
 
 接下来对预处理生成的numOper.i和hello.i两个文件进行编译,可以使用如下命令:
 # gcc -S hello.i -o hello.s
 # gcc -S numOper.i -o numOper.s
 也可以使用下面的命名将预处理和编译和在一起做,只是把上面的预处理生成的文件直接换成源文件既可以:
 # gcc -S hello.c -o hello.s
 # gcc -S numOper.c -o numOper.s
 最终将生成hello.s和numOper.s两个汇编文件,这两个文件的内容为:
 hello.s文件的内容为:
   .file "hello.c"
  .globl NUM_2
   .section .rodata
   .align 4
   .type NUM_2, @object
   .size NUM_2, 4
  NUM_2:
   .long 15
   .text
  .globl main
   .type main, @function
  main:
   pushl %ebp
   movl %esp, %ebp
   andl $-16, %esp
   subl $16, %esp
   movl NUM_2, %eax
   movl %eax, 4(%esp)
   movl $10, (%esp)
   call add
   movl $0, %eax
   leave
   ret
   .size main, .-main
   .ident "GCC: (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3"
   .section .note.GNU-stack,"",@progbits

 numOper.s文件的内容为:
   .file "numOper.c"
  .text
  .globl add
   .type add, @function
  add:
   pushl %ebp
   movl %esp, %ebp
   movl 12(%ebp), %eax
   movl 8(%ebp), %edx
   leal (%edx,%eax), %eax
   popl %ebp
   ret
   .size add, .-add
   .ident "GCC: (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3"
   .section .note.GNU-stack,"",@progbits
 numOper.s的汇编代码比较简单,从中可以看到,先将函数传入的两个实参放入寄存器eax和edx中,然后对这两个参数进行相加,最后返回结果,这确实是源文件numOper.c中的内容
 
 三、汇编
 
 在编译过程中,编译器会将代码进行重新整理,其中的代码部分编译之后被放到代码段、初始化了常量静态变量之类的将被放到数据段中等等。
 3.1:将第二步生成的汇编代码经过汇编之后变成机器码:
  # gcc -c hello.s -o hello.o
  # gcc -c numOper.s -o numOper.o
 这里,hello.o和numOper.o两个文件都是linux下ELF格式的目标文件,
 目标文件主要包括可执行文件、动态库链接库、静态链接库之类,经过以上步骤也可以看出目标文件中所有的命令已经被处理成机器能够识别的语言了,只是其中的一些重定位工作还没有进行处理。
 linux下目标文件的格式为ELF格式。
   
 3.2:查看目标文件的内容,这里以hello.o中的内容为例:
  可以使用以下命令查看:
  # objdump -s -d hello.o
 下面是对hello.o文件的部分内容进行整理后,分析如下:
  
 3.2.1:代码段.text的内容为(该段为代码段),段中第一列[]括起来的为列为偏移量,紧接之后的才是所在段里面的实际内容::
    [0000] 5589e583 e4f083ec 10a10000 00008944 
    [0010] 2404c704 240a0000 00e8fcff ffffb800
    [0020] 000000c9 c3                                
  
  对.text段中的上述机器码进行反汇编得到的内容为:
   00000000 <main>:
      0:   55                      push   %ebp
      1:   89 e5                   mov    %esp,%ebp
      3:   83 e4 f0                and    $0xfffffff0,%esp
      6:   83 ec 10                sub    $0x10,%esp
      9:   a1 00 00 00 00          mov    0x0,%eax
      e:   89 44 24 04             mov    %eax,0x4(%esp)
     12:   c7 04 24 0a 00 00 00    movl   $0xa,(%esp)
     19:   e8 fc ff ff ff          call   1a <main+0x1a>
     1e:   b8 00 00 00 00          mov    $0x0,%eax
     23:   c9                      leave 
     24:   c3                      ret
   
  其中,对比以下就会发现第二列的55、 89 e5等等 就是.text中的内容,从这里反汇编看到的内容也可以看出跟第二步生成的汇编文件hello.s中的内容比较相似。
  
 3.2.2:常量段.rodata的内容为(该段通常存放一些常量):
   [0000] 0f000000                            
   这里0f000000 就是十六进制表示的 0x0f、0x00、0x00、0x00 也即十进制的15,也就是在源文件hello.c中定义的常量:const int NUM_2 = 15;
  
 3.2.3:段 .comment的内容为(该段存放了编译器的版本相关信息等):
   0000 00474343 3a202855 62756e74 7520342e  .GCC: (Ubuntu 4.
   0010 342e332d 34756275 6e747535 2e312920  4.3-4ubuntu5.1)
   0020 342e342e 3300                        4.4.3.         

  
  
 3.2.4:对numOper.o使用如下命令:
 # objdump -s -d numOper.o
 也可以得到文件numOper.o中内容,下面为不进行分析处理的原始内容,这里不再进行重复分析: 

  numOper.o:     file format elf32-i386

  Contents of section .text:
   0000 5589e58b 450c8b55 088d0402 5dc3      U...E..U....]. 
  Contents of section .comment:
   0000 00474343 3a202855 62756e74 7520342e  .GCC: (Ubuntu 4.
   0010 342e332d 34756275 6e747535 2e312920  4.3-4ubuntu5.1)
   0020 342e342e 3300                        4.4.3.         

  Disassembly of section .text:

  00000000 <add>:
     0:   55                      push   %ebp
     1:   89 e5                   mov    %esp,%ebp
     3:   8b 45 0c                mov    0xc(%ebp),%eax
     6:   8b 55 08                mov    0x8(%ebp),%edx
     9:   8d 04 02                lea    (%edx,%eax,1),%eax
     c:   5d                      pop    %ebp
     d:   c3                      ret   
    
 四、链接 
  链接的内容比较复杂,后续再进行专门的总结。