共计 5831 个字符,预计需要花费 15 分钟才能阅读完成。
导读 | 大家肯定都知道计算机程序设计语言通常分为机器语言、汇编语言和高级语言三类。高级语言需要通过翻译成机器语言才能执行,而翻译的方式分为两种,一种是编译型,另一种是解释型,因此我们基本上将高级语言分为两大类,一种是编译型语言,例如 C,C++,Java,另一种是解释型语言,例如 Python、Ruby、MATLAB、JavaScript。 |
本文将介绍如何将高层的 C /C++ 语言编写的程序转换成为处理器能够执行的二进制代码的过程,包括四个步骤
通常所说的 GCC 是 GUN Compiler Collection 的简称,是 Linux 系统上常用的编译工具。GCC 工具链软件包括 GCC、Binutils、C 运行库等。
GCC(GNU C Compiler)是编译工具。本文所要介绍的将 C /C++ 语言编写的程序转换成为处理器能够执行的二进制代码的过程即由编译器完成。
一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size 等。这一组工具是开发和调试不可缺少的工具,分别简介如下:
addr2line:用来将程序地址转换成其所对应的程序源文件及所对应的代码行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对应的源代码位置。
C 语言标准主要由两部分组成:一部分描述 C 的语法,另一部分描述 C 标准库。C 标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的 printf 函数便是一个 C 标准库函数,其原型定义在 stdio 头文件中。
C 语言标准仅仅定义了 C 标准库函数原型,并没有提供实现。因此,C 语言编译器通常需要一个 C 运行时库(C Run Time Libray,CRT)的支持。C 运行时库又常简称为 C 运行库。与 C 语言类似,C++ 也定义了自己的标准,同时提供相关支持库,称为 C ++ 运行时库。
由于 GCC 工具链主要是在 Linux 环境中进行使用,因此本文也将以 Linux 系统作为工作环境。为了能够演示编译的整个过程,本节先准备一个 C 语言编写的简单 Hello 程序作为示例,其源代码如下所示:
#include
// 此程序很简单,仅仅打印一个 Hello World 的字符串。intmain(void)
{printf("Hello World! \n");
return;
}
预处理的过程主要包括以下过程:
将所有的 #define 删除,并且展开所有的宏定义,并且处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif 等。
处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置。
删除所有注释“//”和“/* */”。
添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
保留所有的 #pragma 编译器指令,后续编译过程需要使用它们。
使用 gcc 进行预处理的命令如下:
$gcc -E hello.c -o hello.i // 将源文件 hello.c 文件预处理生成 hello.i
// GCC 的选项 - E 使 GCC 在进行完预处理后即停止
hello.i 文件可以作为普通文本文件打开进行查看,其代码片段如下所示:
// hello.i 代码片段
externvoidfunlockfile(FILE *__stream)__attribute__((__nothrow__ , __leaf__));
#942"/usr/include/stdio.h"34
#2"hello.c"2
#3"hello.c"
int
main(void)
{printf("Hello World!""\n");
return;
}
编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。
使用 gcc 进行编译的命令如下:
$gcc -S hello.i -o hello.s // 将预处理生成的 hello.i 文件编译生成汇编程序 hello.s
// GCC 的选项 - S 使 GCC 在执行完编译后停止,生成汇编程序
上述命令生成的汇编程序 hello.s 的代码片段如下所示,其全部为汇编代码。
// hello.s 代码片段
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset16
.cfi_offset6,-16
movq %rsp, %rbp
.cfi_def_cfa_register6
movl $.LC0, %edi
call puts
movl $, %eax
popq %rbp
.cfi_def_cfa7,8
ret
.cfi_endproc
汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o 的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用 Binutils 中的汇编器 as 根据汇编指令和处理器指令的对照表一一翻译即可。
当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o 目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。
使用 gcc 进行汇编的命令如下:
$gcc -c hello.s -o hello.o // 将编译生成的 hello.s 文件汇编生成目标文件 hello.o
// GCC 的选项 - c 使 GCC 在执行完汇编后停止,生成目标文件
// 或者直接调用 as 进行汇编
$as -c hello.s -o hello.o // 使用 Binutils 中的 as 将 hello.s 文件汇编生成目标文件
注意:hello.o 目标文件为 ELF(Executable and Linkable Format)格式的可重定向文件。
链接也分为静态链接和动态链接,其要点如下:
$gcc hello.c -o hello
$size hello // 使用 size 查看大小
text data bss dec hex filename
1183 552 8 1743 6cf hello
$ldd hello // 可以看出该可执行文件链接了很多其他动态库,主要是 Linux 的 glibc 动态库
linux-vdso.so.1 => (0x00007fffefd7c000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fadcdd82000)
/lib64/ld-linux-x86-64.so.2 (0x00007fadce14c000)
$gcc -static hello.c -o hello
$size hello // 使用 size 查看大小
text data bss dec hex filename
823726 7284 6360 837370 cc6fa hello // 可以看出 text 的代码尺寸变得极大
$ldd hello
not a dynamic executable // 说明没有链接动态库
链接器链接后生成的最终文件为 ELF 格式可执行文件,一个 ELF 可执行文件通常被链接为不同的段,常见的段譬如.text、.data、.rodata、.bss 等段。
ELF 文件格式如下图所示,位于 ELF Header 和 Section Header Table 之间的都是段(Section)。一个典型的 ELF 文件包含下面几个段:
可以使用 readelf - S 查看其各个 section 的信息如下:
$readelf -S hello
There are 31 section headers, starting at offset 0x19d8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
……
[11] .init PROGBITS 00000000004003c8 000003c8
000000000000001a 0000000000000000 AX 0 0 4
……
[14] .text PROGBITS 0000000000400430 00000430
0000000000000182 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 00000000004005b4 000005b4
……
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包含的指令和数据,需要使用反汇编的方法。
使用 objdump - D 对其进行反汇编如下
$ objdump -D hello
……
0000000000400526 : // main 标签的 PC 地址
//PC 地址:指令编码 指令的汇编格式
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: bf c4 05 40 00 mov $0x4005c4,%edi
40052f: e8 cc fe ff ff callq 400400
400534: b8 00 00 00 00 mov $0x,%eax
400539: 5d pop %rbp
40053a: c3 retq
40053b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
……
使用 objdump - S 将其反汇编并且将其 C 语言源代码混合显示出来:
$ gcc -o hello -g hello.c // 要加上 - g 选项
$ objdump -S hello
……
0000000000400526 :
#include
int
main(void)
{
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
printf("Hello World!" "\n");
40052a: bf c4 05 40 00 mov $0x4005c4,%edi
40052f: e8 cc fe ff ff callq 400400
return ;
400534: b8 00 00 00 00 mov $0x0,%eax
}
400539: 5d pop %rbp
40053a: c3 retq
40053b: f 1f 44 00 00 nopl 0x0(%rax,%rax,1)