本文主要是简单的介绍一下GCC编译工具集中各软件的作用,同时简要介绍一下ELF文件和汇编语言的格式以及如何使用nasm编译器编译.asm文件生成可执行文件。
目录
(一)GCC编译工具集
1.GCC的组成
- GCC(GNU C Compiler)
gcc是编译工具。它可以将 C/C++语言编写的程序转换成为处理器能够执行的二进制代码。GCC 现在还支持 Ada 语言、C++ 语言、Java 语言、Objective C 语言、Pascal 语言、COBOL 语言,以及支持函数式编程和逻辑编程的 Mercury 语言等。而 GCC 也变成了 GNU Compiler Collection 即GNU 编译器家族的意思。 - Binutils: 一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、 ldd、readelf、 size 等。这 一组工具是开发和调试不可缺少的工具 。
工具 | 作用 |
---|---|
addr2line | 用来将程序地址转换成其所对应的程序源文件及所对应的代码行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对应的源代码位置 |
as | 主要用于汇编 |
ld | 主要用于链接 |
ar | 主要用于创建静态库 |
ldd | 可以用于查看一个可执行程序依赖的共享库 |
objcopy | 将一种对象文件翻译成另一种格式,譬如将.bin 转换成.elf、或者将.elf 转换成.bin 等。 |
objdump | 主要的作用是反汇编 |
readelf | 显示有关ELF文件的信息 |
size | 列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等 |
- C运行库
C 语言标准主要由两部分组成:一部分描述C的语法,另一部分描述C标准库。 C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的printf函数便是一个 C标准库函数,其原型定义在stdio头文件中。 C语言标准仅仅定义了C标准库函数原型,并没有提供实现。因此,C语言编译器通常需要一个C运行时库(C Run Time Libray,CRT)的支持。C 运行时库又常简称为 C运行库。与 C语言类似,C++也定义了自己的标准,同时提供相关支持库,称为C++运行时库。
2.GCC编译过程
编译阶段 | 编译命令 | 作用 |
---|---|---|
预处理 | gcc -E test.c -o test.i | 编译器将源代码中包含头文件编译进来 |
编译 | gcc -S test.i -o test.s | 检查代码规范性并翻译成汇编语言 |
汇编 | gcc -c test.s -o test.o | 将.s文件转换为目标文件 |
链接 | gcc test.o -o test | 将目标文件转换为可执行文件 |
-
预处理
执行命令gcc -E test.c -o test.i
,生成test.i文件
test.i部分代码:
-
编译
执行命令gcc -S test.i -o test.s
,生成test.s文件
test.s部分代码:
-
汇编
执行命令gcc -c test.s -o test.o
,生成test.o文件
-
链接
执行命令gcc test.o -o test
,生成test可执行文件
(二)EFF文件格式
1.ELF文件
- ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。1
- ELF文件格式如下图,位于ELF Header和Section Header Table 之间的都是段(Section)。一个典型的ELF文件包含下面几个段:
- .text:已编译程序的指令代码段。
- .rodata:ro 代表 read only,即只读数据(譬如常数 const)。
- .data:已初始化的 C 程序全局变量和静态局部变量。
- .bss:未初始化的 C 程序全局变量和静态局部变量。
- .debug:调试符号表,调试器用此段的信息帮助调试。
- 使用
readelf -S test
查看test可执行文件各个section的信息
There are 29 section headers, starting at offset 0x1928:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000238 00000238
000000000000001c 0000000000000000 A 0 0 1
··································································
[26] .symtab SYMTAB 0000000000000000 00001040
00000000000005e8 0000000000000018 27 43 8
[27] .strtab STRTAB 0000000000000000 00001628
0000000000000202 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 0000182a
00000000000000fe 0000000000000000 0 0 1
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
- ELF文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF文件包含的指令和数据,需要使用反汇编的方法,命令为
objdump -D test
test: 文件格式 elf64-x86-64
Disassembly of section .interp:
0000000000000238 <.interp>:
238: 2f (bad)
239: 6c insb (%dx),%es:(%rdi)
23a: 69 62 36 34 2f 6c 64 imul $0x646c2f34,0x36(%rdx),%esp
241: 2d 6c 69 6e 75 sub $0x756e696c,%eax
246: 78 2d js 275 <_init-0x273>
248: 78 38 js 282 <_init-0x266>
24a: 36 2d 36 34 2e 73 ss sub $0x732e3436,%eax
250: 6f outsl %ds:(%rsi),(%dx)
251: 2e 32 00 xor %cs:(%rax),%al
··································································
000000000000063a <main>:
63a: 55 push %rbp
63b: 48 89 e5 mov %rsp,%rbp
63e: 48 8d 3d 9f 00 00 00 lea 0x9f(%rip),%rdi # 6e4 <_IO_stdin_used+0x4>
645: e8 c6 fe ff ff callq 510 <puts@plt>
64a: b8 00 00 00 00 mov $0x0,%eax
64f: 5d pop %rbp
650: c3 retq
651: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
658: 00 00 00
65b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
·····································································
- 使用
objdump -S
将其反汇编并且将其C语言源代码混合显示出来:
执行命令gcc -o hello -g hello.c
再执行objdump -S test
(注意:需要加上参数-g,否则不能显示出c语言的源代码)
······························································
000000000000063a <main>:
#include<stdio.h>
int main(void)
{
63a: 55 push %rbp
63b: 48 89 e5 mov %rsp,%rbp
printf("Hello World!\n");
63e: 48 8d 3d 9f 00 00 00 lea 0x9f(%rip),%rdi # 6e4 <_IO_stdin_used+0x4>
645: e8 c6 fe ff ff callq 510 <puts@plt>
return 0;
64a: b8 00 00 00 00 mov $0x0,%eax
}
64f: 5d pop %rbp
650: c3 retq
651: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
658: 00 00 00
65b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
·······························································
(三)汇编语言格式
1.汇编语言定义
汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。2
2.汇编语言组成
- 传送指令
包括通用数据传送指令MOV、条件传送指令CMOVcc、堆栈操作指令PUSH/PUSHA/PUSHAD/POP/POPA/POPAD、交换指令XCHG/XLAT/BSWAP、地址或段描述符选择子传送指令LEA/LDS/LES/LFS/LGS/LSS等。 - 逻辑运算
这部分指令用于执行算术和逻辑运算,包括加法指令ADD/ADC、减法指令SUB/SBB、加一指令INC、减一指令DEC、比较操作指令CMP、乘法指令MUL/IMUL、除法指令DIV/IDIV、符号扩展指令CBW/CWDE/CDQE、十进制调整指令DAA/DAS/AAA/AAS、逻辑运算指令NOT/AND/OR/XOR/TEST等。 - 移位指令
这部分指令用于将寄存器或内存操作数移动指定的次数。包括逻辑左移指令SHL、逻辑右移指令SHR、算术左移指令SAL、算术右移指令SAR、循环左移指令ROL、循环右移指令ROR等。 - 位操作
这部分指令包括位测试指令BT、位测试并置位指令BTS、位测试并复位指令BTR、位测试并取反指令BTC、位向前扫描指令BSF、位向后扫描指令BSR等。 - 控制转移
这部分包括无条件转移指令JMP、条件转移指令JCC/JCXZ、循环指令LOOP/LOOPE/LOOPNE、过程调用指令CALL、子过程返回指令RET、中断指令INTn、INT3、INTO、IRET等。 - 串操作
这部分指令用于对数据串进行操作,包括串传送指令MOVS、串比较指令CMPS、串扫描指令SCANS、串加载指令LODS、串保存指令STOS,这些指令可以有选择地使用REP/REPE/REPZ/REPNE和REPNZ的前缀以连续操作。 - 输入输出
这部分指令用于同外围设备交换数据,包括端口输入指令IN/INS、端口输出指令OUT/OUTS。
(四)nasm汇编编译器编译生成可执行程序
1.Ubuntu安装nasm汇编编译器
- 先判断是否安装nams
使用命令whereis nasm
,如果显示nasm: /usr/bin/nasm ,则已经安装;如果只显示nasm: ,则未安装。 - 安装nasm
使用命令sudo apt install nasm
安装nasm编译器
如无法安装可以参考linux下的汇编环境搭建(nasm)
2.编译代码hello.asm
- hello.asm源代码
; hello.asm
section .data ; 数据段声明
msg db "Hello, world!", 0xA ; 要输出的字符串
len equ $ - msg ; 字串长度
section .text ; 代码段声明
global _start ; 指定入口函数
_start: ; 在屏幕上显示一个字符串
mov edx, len ; 参数三:字符串长度
mov ecx, msg ; 参数二:要显示的字符串
mov ebx, 1 ; 参数一:文件描述符(stdout)
mov eax, 4 ; 系统调用号(sys_write)
int 0x80 ; 调用内核功能
; 退出程序
mov ebx, 0 ; 参数一:退出代码
mov eax, 1 ; 系统调用号(sys_exit)
int 0x80 ; 调用内核功能
- 编译生成hello可执行文件
使用命令nasm -f elf64 hello.asm
将hello.asm文件生成hello.o文件
使用命令ld -s -o hello hello.o
将hello.o文件生成hello 可执行文件
执行结果如下:
- 遇到的问题
使用命令nasm -f elf64 hello.asm
生成hello.o文件后再使用命令gcc hello.o -o hello
报错如下:hello.o:在函数‘_start’中: hello.asm:(.text+0x0): `_start'被多次定义 /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o:(.text+0x0):第一次在此定义 /usr/bin/ld: hello.o: relocation R_X86_64_32 against `.data' can not be used when making a PIE object; recompile with -fPIC /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o:在函数‘_start’中:(.text+0x20):对‘main’未定义的引用 /usr/bin/ld: 最后的链结失败: 无效的操作 collect2: error: ld returned 1 exit status
- 原因:
gcc编译需要链接c语言标准库,而用nasm生成的.o文件没有用到c语言的标准库,因而使用gcc编译时会报错 - 解决方法
将_start改为main即可
- 原因:
- 遇到的问题
- 与gcc编译生成的可执行文件对比(功能都是输出
Hello,world!
)- nasm汇编语言生成的可执行代码大小为512字节
- gcc编译生成的可执行代码大小为8296字节
可见使用nasm汇编生成的main可执行文件要比gcc编译生成的可执行文件要更小。
- nasm汇编语言生成的可执行代码大小为512字节
(五)总结
通过对gcc工具集的使用,我了解了在一个程序编译过程中是如何一步一步的编译成可执行文件的。除此之外,还了解了ELF文件和汇编文件的格式以及如何用nasm汇编编辑器编译hello.asm文件生成可执行文件hello。以上这些实验,使我对于Linux操作系统上的程序编译的理解更加深入。