汇编的教程(四)- 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 为 后缀 |
| 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
此文件的内容为二进制格式,可以使用hexdump或od运行以下任一命令来检查:
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 选项

浙公网安备 33010602011771号