大家肯定都知道计算机程序设计语言通常分为机器语言、汇编语言和高级语言三类。高级语言需要通过翻译成机器语言才能执行,而翻译的方式分为两种,一种是编译型,另一种是解释型,因此我们基本上将高级语言分为两大类,一种是编译型语言,例如C,C++,Java,另一种是解释型语言,例如Python、Ruby、MATLAB 、JavaScript。
本文将介绍如何将高层的C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程,包括四个步骤:
GCC 工具链介绍
通常所说的GCC是GUN Compiler Collection的简称,是Linux系统上常用的编译工具。GCC工具链软件包括GCC、Binutils、C运行库等。
GCC
GCC(GNU C Compiler)是编译工具。本文所要介绍的将C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程即由编译器完成。
Binutils
一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。这一组工具是开发和调试不可缺少的工具,分别简介如下:
C运行库
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 <stdio.h>
- //此程序很简单,仅仅打印一个Hello World的字符串。
- int main(void)
- {
- printf("Hello World! \n");
- return 0;
- }
编译过程
1.预处理
预处理的过程主要包括以下过程:
- $ gcc -E hello.c -o hello.i // 将源文件hello.c文件预处理生成hello.i
- // GCC的选项-E使GCC在进行完预处理后即停止
hello.i文件可以作为普通文本文件打开进行查看,其代码片段如下所示:
- // hello.i代码片段
- extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
- # 942 "/usr/include/stdio.h" 3 4
- # 2 "hello.c" 2
- # 3 "hello.c"
- int
- main(void)
- {
- printf("Hello World!" "\n");
- return 0;
- }
2.编译
编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。
使用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_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
3.汇编
汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.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)格式的可重定向文件。
4.链接
链接也分为静态链接和动态链接,其要点如下:
由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动态库文件,比如libtest.a和libtest.so,gcc链接时默认优先选择动态库,会链接libtest.so,如果要让gcc选择链接libtest.a则可以指定gcc选项-static,该选项会强制使用静态库进行链接。以Hello World为例:
- $ 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文件
1.ELF文件的段
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
- ……
2.反汇编ELF
由于ELF文件无法被当做普通文本文件打开,如果希望直接查看一个ELF文件包含的指令和数据,需要使用反汇编的方法。
使用objdump -D对其进行反汇编如下:
- $ objdump -D hello
- ……
- 0000000000400526 <main>: // 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 <puts@plt>
- 400534: b8 00 00 00 00 mov $0x0,%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 <main>:
- #include <stdio.h>
- 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 <puts@plt>
- return 0;
- 400534: b8 00 00 00 00 mov $0x0,%eax
- }
- 400539: 5d pop %rbp
- 40053a: c3 retq
- 40053b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- ……
一、脚本 今天主要分享一个shell脚本,用来获取linux系统CPU、内存、磁盘IO等信...
用PHP的json_encode来处理中文的时候, 中文都会被编码, 变成不可读的, 类似”\u*...
fMRI数据的处理步骤需要三个工具 SPM12, DPARSF工具箱, 分析工具REST 保持磁化平...
1.写在前面: 阅读要求: 具有一定的HTML、CSS、JavaScript、Json基础 2.什么是a...
前言 本篇主要记录微信支付中公众号及H5支付全过程。 准备篇 公众号或者服务号(...
最近在整理优化.net代码时,发现几个很不友好的处理现象:登录判断、权限认证、...
textarea在保存时格式是可以保存到数据库的,但是展示时因为/n和不能互转导致页...
request介绍 Requests建立在世界上下载量最大的Python库urllib3上它令Web请求变...
线代矩阵 不要忘了改类名 ----4月4日 package com . hjh . function ; import ja...
本篇文章主要介绍了Android性能优化之利用LeakCanary检测内存泄漏及解决办法有兴...