大家一定知道,计算机编程语言通常分为机器语言、汇编语言、高级语言三大类。高级语言需要翻译成机器语言才能执行,而翻译的方式有两种,一种是编译式,一种是解释式,所以我们基本上把高级语言分为两类,一种是编译式类型语言,如C、C++、Java,另一种是解释型语言,如Python、Ruby、MATLAB、JavaScript。本文将介绍如何将用高级C/C++语言编写的程序转换成处理器可以执行的二进制代码,包括四个步骤:预处理(Preprocessing)编译(Compilation)汇编(Assembly)链接(Linking)GCC工具链简介通常所说的GCC是GUNCompilerCollection的缩写,是Linux系统上常用的编译工具。GCC工具链软件包括GCC、Binutils、C运行时库等,GCCGCC(GNUCCompiler)是一个编译工具。将C/C++语言编写的程序转换成处理器可以执行的二进制代码的过程是由编译器完成的。Binutils是一套二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等,这组工具是开发调试不可缺少的工具。介绍如下:addr2line:用于将程序地址转换成其对应的程序源文件和对应的代码行,也得到对应的函数。该工具将帮助调试器在调试过程中定位到相应的源代码位置。as:主要用于汇编,关于汇编的详细介绍请参考下文。ld:主要用于链接。有关链接的详细信息,请参阅以下文本。ar:主要用来创建静态库。为了方便初学者,这里介绍一下动态库和静态库的概念:如果要将多个.o目标文件生成一个库文件,库有两种,一种是静态库,一种是动态库。在Windows中,静态库是后缀为.lib的文件,共享库是后缀为.dll的文件。在Linux中,静态库是一个后缀为.a的文件,共享库是一个后缀为.so的文件。静态库和动态库的区别在于代码加载的时刻不同。静态库的代码在编译过程中已经加载到可执行程序中,所以体积比较大。共享库的代码在可执行程序运行时加载到内存中,在编译过程中只是简单地引用,因此代码量小。在Linux系统中,可以使用ldd命令查看某个可执行程序所依赖的共享库。如果一个系统中有多个程序需要同时运行,并且这些程序之间存在共享库,那么使用动态库会更节省内存。ldd:可以用来查看一个可执行程序所依赖的共享库。objcopy:将目标文件翻译成另一种格式,如.bin转.elf,或.elf转.bin等。objdump:主要作用是反汇编。反汇编的详细介绍见下文。readelf:显示有关ELF文件的信息,更多信息见后文。size:列出可执行文件各部分的大小和总大小,代码段,数据段,总大小等,使用size的具体使用例子请参考下文。C运行时库C语言标准主要由两部分组成:一部分描述了C的语法,另一部分描述了C标准库。C标准库定义了一组标准的头文件,每个头文件包含一些相关的函数、变量、类型声明和宏定义。比如常见的printf函数就是一个C标准库函数,其原型定义在stdio头文件中。C语言标准只定义了C标准库函数的原型,不提供实现。因此,C语言编译器通常需要C运行时库(CRunTimeLibrary,CRT)的支持。C运行时库通常简称为C运行时库。与C语言类似,C++也定义了自己的标准,并提供了相关的支持库,称为C++运行时库。准备工作由于GCC工具链主要在Linux环境下使用,因此本文也将使用Linux系统作为工作环境。为了演示整个编译过程,本节首先准备一个用C语言编写的简单的Hello程序作为例子,其源代码如下:#include//这个程序很简单,它只打印一个HelloWorld字符串。intmain(void){printf("HelloWorld!\n");return0;}编译过程1.预处理预处理过程主要包括以下过程:删除所有#define,展开所有宏定义,处理所有条件预编译指令,如如#if#ifdef#elif#else#endif等。处理#include预编译指令,将包含的文件插入到预编译指令的位置。删除所有注释“//”和“/**/”。添加行号和文件标识符,在编译时生成调试行号和编译错误警告行号。保留后续编译所需的所有#pragma编译器指令。使用gcc进行预处理的命令如下:$gcc-Ehello.c-ohello.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"intmain(void){printf("HelloWorld!""\n");return0;}2.编译编译过程是预处理经过一系列的词法分析、语法分析、语义分析和优化,生成完成的文件,生成相应的汇编代码。用gcc编译的命令如下:$gcc-Shell.i-ohello.s//编译预处理生成的hello.i文件,生成汇编程序hello.s//GCC的??选项-S使GCC执行后compilingStop,generateanassembler上面命令生成的汇编程序hello.s的代码片段如下,都是汇编代码。//hello.s代码片段main:.LFB0:.cfi_startprocpushq%rbp.cfi_def_cfa_offset16.cfi_offset6,-16movq??%rsp,%rbp.cfi_def_cfa_register6movl$.LC0,%edcallputsmovl$0,%eaxpopq%rbp.cfi_def_cfa7,8ret.cfi_endpro程序集processcall对汇编代码进行处理,生成处理器可以识别的指令,保存在后缀为.o的目标文件中。由于每条汇编语句几乎都对应一条处理器指令,因此汇编过程相对于编译过程来说相对简单,只要按照汇编指令和处理器指令对照表调用Binutils中的汇编器就可以一一翻译。当程序由多个源代码文件组成时,每个文件首先要完成汇编工作,只有生成.o目标文件后才能进入下一步的链接工作。注意:目标文件已经是最终程序的一部分,但在链接之前不能执行。用gcc进行汇编的命令如下:$gcc-chello.s-ohello.o//将编译生成的hello.s文件进行汇编,生成目标文件hello.o//GCC的??选项-c使GCC停止执行完程序集,生成目标文件//或者直接调用asforassembly$as-chello.s-ohello.o//使用Binutils中的as汇编hello.s文件生成目标文件注意:hello.o目标文件是ELF(可执行和可链接格式)格式的可重定向文件。4.链接链接也分为静态链接和动态链接。要点如下:静态链接是指在编译阶段直接将静态库添加到可执行文件中,这样可执行文件会比较大。链接器将函数的代码从其位置(在不同的目标文件中或在静态链接库中)复制到最终的可执行程序中。为了创建可执行文件,链接器必须完成的主要任务是:符号解析(将目标文件中符号的定义和引用关联起来)和重定位(将符号定义对应到内存地址,然后修改所有引用到符号)。动态链接是指在链接阶段只加入一些描述信息,程序执行时从系统加载相应的动态库到内存中。在Linux系统中,gcc编译链接时动态库搜索路径的顺序通常是:先从gcc命令的参数-L指定的路径开始搜索;然后从环境变量LIBRARY_PATH指定的路径中寻址;然后从默认路径/lib、/usr/lib、/usr/local/lib寻找。在Linux系统中,执行二进制文件时动态库搜索路径的顺序通常是:先搜索编译目标代码时指定的动态库搜索路径;然后从环境变量LD_LIBRARY_PATH指定的路径中寻址;然后从配置文件/etc/ld.so.conf中指定的动态库搜索路径;然后从默认路径/lib、/usr/lib搜索。在Linux系统中,可以使用ldd命令查看某个可执行程序所依赖的共享库。由于动态库和静态库的链接路径可能会重叠,如果路径中有同名的静态库文件和动态库文件,比如libtest..so,如果想让gcc选择链接libtest.a,可以指定gcc选项-static,这样会强制使用静态库进行链接。以HelloWorld为例:如果使用命令“gcchello.c-ohello”,将使用动态库进行链接,生成的ELF可执行文件的大小(使用Binutils的size命令查看)和链接的动态库(使用Binutils的ldd命令查看)如下:$gcchello.c-ohello$sizehello//使用size查看大小textdatabssdechexfilename1183552817436cfhello$lddhello//可以看出该可执行文件链接了很多其他的动态库,主要是linux的glibc动态库linux-vdso.so.1=>(0x00007ffffefd7c000)libc.so.6=>/lib/x86_64-linux-gnu/libc.so.6(0x00007fadcdd82000)/lib64/ld-linux-x86-64.so.2(0x00007fadce14c000)如果使用命令"gcc-statichello.c-ohello",静态库将用于链接,生成的ELF可执行文件的大小(使用size命令Binutils的查看)和链接的动态库(使用Binutils的ldd命令查看)如下:$gcc-statichello.c-ohello$sizehello//使用size查看thesizetextdatabssdechexfilename82372672846360837370cc6fahello//可以看出文本的代码大小已经变得很大$lddhellonotadynamicexecutable//说明没有链接到动态库链接器最终生成的文件是一个可执行文件在精灵格式。ELF可执行文件通常链接到不同的段,例如.text、.data、.rodata和.bss。ELF文件分析1、ELF文件的ELF文件格式如下图所示。ELFHeader和SectionHeaderTable之间的部分是部分。典型的ELF文件包含以下部分:.text:已编译程序的指令代码部分。.rodata:ro代表只读,即只读数据(如常量const)。.data:初始化的C程序全局变量和静态局部变量。.bss:未初始化的C程序全局变量和静态局部变量。.debug:调试符号表,调试器使用本节中的信息来帮助调试。…[11].initPROGBITS00000000004003c8000003c8000000000000001a0000000000000000AX004……[14].textPROGBITS00000000004004300000043000000000000001820000000000000000AX0016[15].finiPROGBITS00000000004005b4000005b4…...2.反汇编ELF由于ELF文件不能像普通文本文件一样打开,所以如果想直接查看ELF文件中包含的指令和数据,就需要使用反汇编的方法。使用objdump-D反汇编如下:$objdump-Dhello...0000000000400526://主标签的PC地址//PC地址:指令编码指令汇编格式400526:55push%rbp400527:4889e5mov%rsp,%rbp40052a:bfc4054000mov$0x4005c4,%edi40052f:e8ccfeffffcallq400400400534:b800000000mov$0x0,%eax400539:5dpop%rbp40053a:c3retq40053b:0f1f440000nopl0x0(%rax,%rax,1)……使用objdump-S将其反Assemble并显示其C语言源代码:$gcc-ohello-ghello.c//Add-goption$objdump-Shell...0000000000400526:#includeintmain(void){400526:55push%rbp400527:4889e5mov%rsp,%rbpprintf("HelloWorld!""\n");40052a:bfc4054000mov$0x4005c4,%edi40052f:e8ccfeffffcallq400400return0;400534:b800000000mov$0x0,%0dpopeax3}:c3retq40053b:0f1f440000nopl0x0(%rax,%rax,1)...