汇编的教程(四)- linux 下c程序的编译四个过程

我们写汇编最好也了解下一个高级程序的整个编译到执行过程,我用C语言的例子来阐述整个过程,从而对比汇编的一些内容。

实验环境:

[root@ht5 testc]# uname -a    //内核版本
Linux ht5.node 3.10.0-1160.42.2.el7.x86_64 #1 SMP Tue Sep 7 14:49:57 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
[root@ht5 testc]# cat /etc/redhat-release  //操作系统 
CentOS Linux release 7.9.2009 (Core)
[root@ht5 testc]# as -v  //即gas汇编器
GNU assembler version 2.27 (x86_64-redhat-linux) using BFD version version 2.27-44.base.el7
[root@ht5 testc]# gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
....
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) 

 

预备知识: 

 1、GCC(GNU C Compiler)是编译工具集合。         

 2、关于gcc、glibc和 binutils  

  1)、gcc(gnu collect compiler)是编译工具集的总称。

  2)、binutils提供了一系列用来创建、管理和维护二进制目标文件的工具程序,如Gas汇编(as)、链接器(ld)、静态库归档(ar)、反汇编工具(objdump)、elf格式文件分析工具(readelf)、无效调试信息和符号的工具(strip)等。通常,binutils与gcc是紧密相集成的,没有binutils的话,gcc是不能正常工作的。

   3)、glibc是gnu发布的libc库,也即c运行库。glibc是linux系统中最底层的api(应用程序开发接口),GNU C库项目(gnu的项目列表查看 https://www.gnu.org/software/) 为GNU系统和GNU/Linux系统以及许多其他使用Linux作为内核的系统提供了核心库。这些库提供了包括ISO C11、POSIX.1-2008、BSD、特定于操作系统的API等。这些API包括像open、read、write、malloc、printf、getaddrinfo、dlopen、pthread create、crypt、login、exit等基础的apis。

     什么是glibc 请看这里

 

一、GCC
    全称为
GNU Compiler Collection即 GNU 编译器集合

GNU Compiler Collection  包括C、 C++、Objective-C、Fortran 、Ada、Go 和 D的前端 ,以及这些语言的库(libstdc++,...)。GCC 最初是作为GNU 操作系统的编译器编写的。GNU 系统被开发成 100% 自由软件,自由是因为它尊重用户的自由

 

      GCC官方网站:  https://gcc.gnu.org/ 
     各个版本:  https://gcc.gnu.org/releases.html
      gcc在线文档: https://gcc.gnu.org/onlinedocs/

 

 二、GNU Binutils工具集
                 官网: https://www.gnu.org/software/binutils/

GNU Binutils 是一系列的二进制工具的集合
主要  
ldI(Gld) GNU 链接器,有关链接的详细介绍请参见后文
as(Gas) GNU Assembler,通常称为gas或as,是由GNU项目开发的汇编程序。
GAS第一个版本的 在1986-1987年发布,由Dean Elsner编写,当时支持的是VAX架构
有关汇编的详细介绍
https://en.wikipedia.org/wiki/GNU_Assembler
 https://ee209-2019-spring.github.io/references/gnu-assembler.pdf (2.14版本)
 https://astro.uni-bonn.de/~sysstw/CompMan/gnu/as.html (1994年,你可以看到很多支持的其他架构)
 http://web.mit.edu/gnu/doc/html/as_1.html ( 这个手册是针对GNU assembler as的指南.)
 
 注意:
1)汇编器(Assembler)是将汇编语言翻译为机器语言的程序。一般而言,汇编生成的是目标代码,需要经gnu 链接器(ldl)生成可执行代码才可以执行
2) GAS,即 GNU 汇编器,是 GNU 操作系统的默认的汇编器。它不仅仅是支持x86架构,它适用于许多不同的体系结构并支持多种汇编语言语法。
   
3)默认是作为gcc后端工具来使用,也就是不是直接调用 as 命令来执行编译,是由其他工具来调用
4)默认语法是AT&T语法
5)支持两种基本注解 /**/ 和 #
   arm架构用 @作为注解
   i386架构,x86-64架构,mips架构,risc-v架构 等都采用 #
   AArch64架构采用 //
gold a new, faster, ELF only linker.
其他  
addr2line

用 来将程序 地址转 换成其所 对应的程 序源文 件及所对 应的代 码行,也可以得到所对应的函数。

ar

可以对静态库做创建、修改和提取的操作

1) windows static lib 以 .lib 为 后缀 的文 件 ,share lib 以 .dll 为 后缀
2)linux static lib 以 .a 为 后 缀 的 文 件 , share lib 以 .so 为 后 缀
   静 态 库 和 动 态 库 的 不 同 点 :
    1)static lib 的 代 码 在 编译 过 程 中 已 经 被 载 入 可 执 行 程 序 , 因 此 体 积 较 大 。 
    2)share lib 的 代 码 是 在 可 执行 程 序 运 行 时 才 载 入 内 存 的 

c++filt  反编译(反混淆,demangle)C++符号的工具
dlltool  创建创建Windows动态库
elfedit  
gprof  性能分析(profiling)工具程序
gprofng  
nlmconv  可以转换成NetWare Loadable Module(NLM)目标文件格式
nm  显示目标文件内的符号信息
objcopy

将一种对象文件翻译成另一种格式,譬如将.bin 转换成.elf、或者将.elf 转换成.bin 等。

objdump 主要的作用是反汇编。显示目标文件的相关信息
ranlib  产生静态库的索引
readelf 显示有关 ELF 文件的信息
size 列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等,请参见后文了解使用 size 的具体使用实例
strings  列出文件中可打印的字符串信息
strip  从目标文件中移除符号信息
windmc  Windows消息资源编译器
windres  Windows资源文件编译器
 ldd  可以用于查看一个可执行程序依赖的share lib。

 

 三、编译 C 程序四个阶段过程。
 总体而言,该过程可以分为四个独立的阶段:Preprocessing(
预处理)、Compilation(编译)、 Assembly(汇编)和 Linking(链接)

 

  1、Preprocessing (预处理)

  编译的第一阶段称为预处理。这个步骤的目标文件为.i ,在此阶段,以#字符开头的行被解释为预处理器命令。这些命令形成了一种简单的宏语言,具有自己的语法和语义。这种语言用于通过提供内联文件、定义宏和有条件地省略代码的功能来减少源代码中的重复。在解释命令之前,预处理器会进行一些初始处理。这包括连接续行(以 结尾的行\)和删除注释。

    具体处理过程如下:

    1) 删除注释

//注释将在第一阶段被删除掉
#include <stdio.h>
int main(void) { puts("test!"); return 0; }

     2)宏展开
    宏是 C 语言中使用#define指令定义的一些常量值或表达式。宏调用导致宏扩展。预处理器创建一个中间文件,其中一些预先编写的汇编级指令替换定义的表达式或常量(基本上匹配标记)。为了区分原始指令和宏扩展产生的汇编指令,在每个宏扩展语句中添加一个“+”号。

    3)文件包含
     C语言中的文件包含是在预处理过程中将另一个包含一些预先编写的代码的文件添加到我们的C程序中。它是使用#include指令完成的。预处理期间的文件包含导致文件名的全部内容被添加到源代码中,替换#include<filename>指令。例如上面的:
       #include <stdio.h>
     4)条件编译
     检查是否定义了宏(常量值或使用#define定义的表达式)后,条件编译正在运行或避免代码块。
预处理器用一些预定义的汇编代码替换所有条件编译指令,并将新扩展的文件传递给编译器。可以在 C 程序中使用#ifdef、#endif、#ifndef、#if、#else和#elif等命令执行条件编译。
     例如:

#include <stdio.h>
int main()
{
    #ifdef AOZHEJIN
        printf("it is aozhejin");
    #else
        printf("it is not aozhejin");
    #endif
    return 0;
}

  下面我们就来生成中间文件test.i  (-E 表示处理阶段结束后停止,以便我们更好的分析 生成的test.i文件)

[root@ht5 testc]# gcc -E test.c -o test.i
[root@ht5 testc]# ls
test.c  test.i
[root@ht5 testc]# file test.i #这步得到的是一个文本文件 test.i: C source, ASCII text

    test.i 文件其代码片段如下所示:

[root@ht5 testc]# cat test.i ....

extern int pclose (FILE *__stream);

extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 913 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));

extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;

extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 943 "/usr/include/stdio.h" 3 4


# 3 "test.c" 2
int
main(void)
{
puts("test!");
return 0;
}

..... 

  我们看下文件的大小 

[root@ht5 testc]# ll
total 24
-rw-r--r-- 1 root root    66 Mar  4 19:32 test.c
-rw-r--r-- 1 root root 16864 Mar  4 19:37 test.i
[root@ht5 testc]# ll -h total 24K -rw-r--r-- 1 root root 66 Mar 4 19:32 test.c -rw-r--r-- 1 root root 17K Mar 4 19:37 test.i
 

   我们上面演示过程就是逐步的分析一个c程序最后是怎么变成可执行文件的,所以我们没有跨过这个过程

2、Compilation(编译)

   这个步骤目标文件为.s, C 中的编译阶段使用c编译器软件将中间 ( .i ) 文件转换为具有汇编级指令(低级代码)的汇编文件 .s 。这是为提高程序的性能,c编译器将中间文件翻译成汇编文件。汇编代码是一种简单的人类可读的语言,用于编写低级指令(在嵌入式或工控器件开发中应用比较多)。整个程序代码由编译器软件一次性解析(语法分析),并通过终端窗口告诉我们源代码中存在的任何语法错误或警告。此步骤允许 C 代码包含内联汇编指令并允许使用不同的汇编程序。一些编译器还支持使用集成汇编器,在编译阶段直接生成机器码,避免了生成中间汇编指令和调用汇编器的开销

     我们生成中间的.s文件 (-S 在适当的编译阶段后停止;不要汇编。(即不生成.o)

     我们可以从 *.c文件或*.i文件,输出汇编文件*.s  (可以看下我的另一篇文章 )

[root@ht5 testc]# gcc -S test.i -o test.s  //这里我是用了上面生成的.i文件
[root@ht5 testc]# ll
total 28
-rw-r--r-- 1 root root    66 Mar  4 20:22 test.c
-rw-r--r-- 1 root root 16864 Mar  4 19:37 test.i
-rw-r--r-- 1 root root   441 Mar  4 20:24 test.s  //44字节
你也可以使用下面的,是等同的
[root@ht5 testc]# gcc -S -m64 test.c //-m64为生成 64 位程序集的指令

   linux gcc编译后的 test.s 内容如下: 

[root@ht5 testc]# cat test.s
    .file    "test.c"
    .section    .rodata
.LC0:
    .string    "test!"  
    .text
    .globl    main
    .type    main, @function
main:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size    main, .-main
    .ident    "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
    .section    .note.GNU-stack,"",@progbits

  windows 下gcc编译后的test.s如下:

    .file    "test.c"
    .text
    .def    __main;    .scl    2;    .type    32;    .endef
    .section .rdata,"dr"
.LC0:
    .ascii "test!\0"
    .text
    .globl    main
    .def    main;    .scl    2;    .type    32;    .endef
    .seh_proc    main
main:
    pushq    %rbp
    .seh_pushreg    %rbp
    movq    %rsp, %rbp
    .seh_setframe    %rbp, 0
    subq    $32, %rsp
    .seh_stackalloc    32
    .seh_endprologue
    call    __main
    leaq    .LC0(%rip), %rcx
    call    puts
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident    "GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 7.3.0"
    .def    puts;    .scl    2;    .type    32;    .endef

解释test.s 汇编

 .file    "test.c"  //这行用于调试,可以排除在外.
    .section    .rodata   //.rodata 代表只读数据
.LC0:
    .string    "test!"  //注意这里和ascii,asciiz 等的区别
    .text
    .globl    main  //注意这个标签main不能理解为汇编的全局标签,这个main是代表会进入c语言的main开始代码
    .type    main, @function 
//c语言的int main
main:
.LFB0:
    .cfi_startproc
    pushq    %rbp  //pushq 为AT&T 格式,intel为push  
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp 
    .cfi_def_cfa_register 6
    movl    $.LC0, %edi
//结束 int main
//开始 puts 函数
    call    puts
//结束puts 函数
//return 0
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
//return0
    .cfi_endproc
.LFE0:
    .size    main, .-main
    .ident    "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
    .section    .note.GNU-stack,"",@progbits
//整个汇编文件实际是汇编调用c库的过程。

 

3、Assembling
  在此阶段,使用汇编器将汇编指令翻译成目标代码。输出包含要由目标处理器运行的实际指令,这个阶段的目标文件为.o

  $ gcc -c test.s -o test.o

  // 将编译生成的 hello.s 文件汇编生成目标文件 hello.o

  此文件的内容为二进制格式,可以使用hexdumpod运行以下任一命令来检查:

  gcc 为我们调用了汇编器 (as) 

 

4.Linker 链接  链接器 (ld)

在汇编阶段生成的目标代码由处理器理解的机器指令组成,但程序的某些部分出现了乱序或丢失。要生成可执行程序,必须重新排列现有的部分并填补缺失的部分。这个过程称为链接。链接也分为静态链接和动态链接,其要点如下:
 1) 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。
   链接器将函数的代码从其所在地(不同的目标文件或静态链接库中)拷贝到最终的可执行程序中

  2) 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。

   链接器链接后生成的最终文件为 ELF 格式可执行文件,一个 ELF 可执行文件通常被链接为不同的段,常见的段譬如.text、.data、.rodata、.bss 等段。

   生成可执行程序目标文件

[root@ht5 testc]#  gcc test.c -o testexe  
[root@ht5 testc]# ll total
48 -rw-r--r-- 1 root root 66 Mar 4 20:22 test.c -rwxr-xr-x 1 root root 8360 Mar 4 23:41 testexe -rw-r--r-- 1 root root 16864 Mar 4 19:37 test.i -rw-r--r-- 1 root root 1496 Mar 4 23:36 test.o -rw-r--r-- 1 root root 441 Mar 4 23:17 test.s -rw-r--r-- 1 root root 441 Mar 4 20:24 test.s.bak

[root@ht5 testc]#  size hello  //使用 size 查看大小

[root@ht5 testc]# size testexe   //.text / .data .bss等都有
   text       data        bss        dec        hex    filename
   1220        548          4       1772        6ec    testexe
[root@ht5 testc]# file testexe testexe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=848436fbacac4fb980e7175befd0e68f7bec5946, not stripped


1、动态库进行衔接测试 [默认]
[root@ht5 testc]#
 ldd testexe  //可以看出该可执行文件链接了很多其他动态库,主要是 Linux 的 glibc

[root@ht5 testc]#  ldd testexe
    linux-vdso.so.1 =>  (0x00007ffd9cd6a000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f64ab831000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f64abbff000)

 2、静态库进行链接测试 [-static 参数]
   这种方式下,会编译到静态文件中,文件大小会增加很多,所以要注意。

[root@ht5 testc]# gcc -static test.c -o testexe-static
[root@ht5 testc]# size testexe-static text data bss dec hex filename 770936 6196 8640 785772 bfd6c testexe-static
[root@ht5 testc]# ldd testexe-static not a dynamic executable //不是一个动态可执行的
[root@ht5 testc]# file testexe-static testexe-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=9220a9697505acb8528fc82448bef600283d53a5, not stripped

四、分析 ELF 文件
1.ELF 文件的segment/section段
    ELF 文件格式如下图所示,位于 ELF Header 和 Section Header Table 之间的都是段(Section)。

   一个典型的 ELF 文件包含下面几个段:

.text:已编译程序的指令代码段。
.rodata:ro 代表 read only,即只读数据(譬如常数 const)。
.data:  已初始化的C程序全局变量和静态局部变量。
.bss:   未初始化的C程序全局变量和静态局部变量。
.debug: 调试符号表,调试器用此段的信息帮助调试。

2)  readelf  -S 查看其各个 section 的信息如下:

[root@ht5 testc]#  readelf -S testexe
There are 30 section headers, starting at offset 0x1928:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400318  00000318
       000000000000003d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400356  00000356
       0000000000000008  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400360  00000360
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400380  00000380
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400398  00000398
       0000000000000048  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         00000000004003e0  000003e0
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400400  00000400
       0000000000000040  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000400440  00000440
       0000000000000182  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         00000000004005c4  000005c4
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         00000000004005d0  000005d0
       0000000000000016  0000000000000000   A       0     0     8
  [16] .eh_frame_hdr     PROGBITS         00000000004005e8  000005e8
       0000000000000034  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000400620  00000620
       00000000000000f4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       6     0     8.....
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

2.反汇编 ELF文件 (objdump)

  objdump -D 对其进行反汇编如下:

[root@ht5 testc]# objdump -D testexe

testexe:     file format elf64-x86-64


Disassembly of section .interp:

0000000000400238 <.interp>:
  400238:    2f                       (bad)  
  400239:    6c                       insb   (%dx),%es:(%rdi)
  40023a:    69 62 36 34 2f 6c 64     imul   $0x646c2f34,0x36(%rdx),%esp
  400241:    2d 6c 69 6e 75           sub    $0x756e696c,%eax
  400246:    78 2d                    js     400275 <_init-0x16b>
  400248:    78 38                    js     400282 <_init-0x15e>
  40024a:    36 2d 36 34 2e 73        ss sub $0x732e3436,%eax
  400250:    6f                       outsl  %ds:(%rsi),(%dx)
  400251:    2e 32 00                 xor    %cs:(%rax),%al

Disassembly of section .note.ABI-tag:

0000000000400254 <.note.ABI-tag>:
  400254:    04 00                    add    $0x0,%al
  400256:    00 00                    add    %al,(%rax)
  400258:    10 00                    adc    %al,(%rax)
  40025a:    00 00                    add    %al,(%rax)
  40025c:    01 00                    add    %eax,(%rax)
  40025e:    00 00                    add    %al,(%rax)
  400260:    47                       rex.RXB
  400261:    4e 55                    rex.WRX push %rbp
  400263:    00 00                    add    %al,(%rax)
  400265:    00 00                    add    %al,(%rax)
  400267:    00 02                    add    %al,(%rdx)
  400269:    00 00                    add    %al,(%rax)
  40026b:    00 06                    add    %al,(%rsi)
  40026d:    00 00                    add    %al,(%rax)
  40026f:    00 20                    add    %ah,(%rax)
  400271:    00 00                    add    %al,(%rax)
    ...
 .....
Disassembly of section .comment:

0000000000000000 <.comment>:
   0:    47                       rex.RXB
   1:    43                       rex.XB
   2:    43 3a 20                 rex.XB cmp (%r8),%spl
   5:    28 47 4e                 sub    %al,0x4e(%rdi)
   8:    55                       push   %rbp
   9:    29 20                    sub    %esp,(%rax)
   b:    34 2e                    xor    $0x2e,%al
   d:    38 2e                    cmp    %ch,(%rsi)
   f:    35 20 32 30 31           xor    $0x31303220,%eax
  14:    35 30 36 32 33           xor    $0x33323630,%eax
  19:    20 28                    and    %ch,(%rax)
  1b:    52                       push   %rdx
  1c:    65 64 20 48 61           gs and %cl,%fs:0x61(%rax)
  21:    74 20                    je     43 <_init-0x40039d>
  23:    34 2e                    xor    $0x2e,%al
  25:    38 2e                    cmp    %ch,(%rsi)
  27:    35 2d 34 34 29           xor    $0x2934342d,%eax

 

    使用 objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:

[root@ht5 testc]# gcc -o hello -g hello.c //要加上-g 选项

 




 
posted @ 2023-03-04 17:46  jinzi  阅读(7)  评论(0)    收藏  举报