Linux程序编译过程
目录:
介绍
语言
计算机程序的语言分为机器语言,汇编语言和高级语言三类。
高级语言通过翻译成机器语言才能执行,翻译又分两种,一种是编译型,另一种是解释型。
编译型语言包括C,C++,Java等,解释型语言包括Python,Ruby,JavaScript等。
C/C++语言的编译
C/C++语言的编译为处理器可以处理的二进制程序代码的过程
- 预处理(Preprocessing)
- 编译(Compilation)
- 汇编(Assembly)
- 链接(Linking)
GCC工具链
GCC是GUN Compiler Collection
的简称,是Linux上的常用编译工具,包括GCC,Binutils,C运行库等
GCC
编译工具,将C/C++语言编写的程序转换成为处理器能够执行的二进制代码
Binutils
一组二进制处理程序,是开发和调试需要的工具
- addr2line 用于将程序地址转化为其程序源文件及对应的代码行,也可以获得对应的函数。工具将帮助调试器在调试过程中定位对应源代码的位置
- as 主要用于汇编
- ld 主要用于链接
- ar 主要用于创建静态库
- ldd 用于查看一个可执行程序依赖的共享库
- objcopy 将一个对象文件翻译成另一种格式,例如将
.bin
转换为.elf
,或者将.elf
转换为.bin
- objdump 主要用于反汇编
- readelf 显示有关ELF文件的信息
- size 列出可执行文件每个部分的尺寸和总尺寸,代码段,数据段,总大小等
C运行库
C语言标准分为描述C语言语法和描述C标准库。
- C标准库中定义了一组标准头文件,每个头文件中包含一些相关的函数,变量,类型声明和宏定义,例如C语言的printf函数就是C标准库函数,其原型定义在stdio头文件中。
- C语言语法仅仅是定义了C标准库的函数原型,并没有提供实现,C语言编译的时候需要一个运行库的支持
编译过程
C语言代码
$ cat helloworld.c
#include <stdio.h>
//此程序很简单,仅仅打印一个Hello World的字符串。
int main(void)
{
printf("Hello World! \n");
return 0;
}
编译过程
预处理
- 将所有的
#define
删除,并展开所有定义的宏,并处理所有条件的预编译指令,比如#if
,#ifdef
,#elif
,#else
和#endif
- 处理
#include
预编译指令,将被包含的文件插入到该预编译指令的位置 - 删除注释
- 添加行号和文件标识,以便编译的时候产生调试用的行号和编译错误警告行号
- 保留所有
#pragma
编译器指令,
预处理命令
gcc -E helloworld.c -o helloworld.i
可以看到预处理生成的文件很大
$ ll | grep hello
-rw-rw-r-- 1 ec2-user ec2-user 153 Sep 26 18:12 helloworld.c
-rw-rw-r-- 1 ec2-user ec2-user 16759 Sep 26 18:12 helloworld.i
底部代码段可以看到我们写入的相关内容
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));
# 938 "/usr/include/stdio.h" 3 4
# 2 "helloworld.c" 2
int main(void)
{
printf("Hello World! \n");
return 0;
}
编译
编译过程是对预处理完成的文件进行一系列的词法分析,语法分析及优化后生成对应的汇编代码。
编译命令
gcc -S helloworld.i -o helloworld.s
-S
参数使GCC在执行编译完成后停止,生成汇编程序
$ ll | grep hello
-rw-rw-r-- 1 ec2-user ec2-user 153 Sep 26 18:12 helloworld.c
-rw-rw-r-- 1 ec2-user ec2-user 16759 Sep 26 18:12 helloworld.i
-rw-rw-r-- 1 ec2-user ec2-user 450 Sep 26 18:14 helloworld.s
编译后的代码
$ cat helloworld.s
.file "helloworld.c"
.section .rodata
.LC0:
.string "Hello World! "
.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
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-18)"
.section .note.GNU-stack,"",@progbits
汇编
汇编过程是对汇编代码进行处理,生成处理器能识别的指令,保存在.o
为后缀的目标文件。
由于每条汇编语句都会对应一条处理器指令,所以汇编过程比较简单,通过调用Binutils中的汇编器as根据汇编指令和处理器指令对照表翻译即可。
$ gcc -c helloworld.s -o helloworld.o
也可以直接使用as命令
$ as -c helloworld.s -o helloworld.o
查看一下生成的文件大小
$ ll | grep hello
-rw-rw-r-- 1 ec2-user ec2-user 153 Sep 26 18:12 helloworld.c
-rw-rw-r-- 1 ec2-user ec2-user 16759 Sep 26 18:12 helloworld.i
-rw-rw-r-- 1 ec2-user ec2-user 1504 Sep 26 18:27 helloworld.o
-rw-rw-r-- 1 ec2-user ec2-user 450 Sep 26 18:12 helloworld.s
helloworld.o
目标文件为ELF(Executable and Linkable Format)格式的可重定向文件
链接
链接分为动态链接和静态链接
- 静态链接 在编译阶段直接把动态链接库加入可执行文件中,这样可执行文件会比较大,链接器将函数的代码从所在地直接拷贝到最终的可执行程序中。为创建可执行文件,链接器必须完成符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)
- 动态链接 在链接阶段仅仅加入一些描述信息,而程序执行的时候再从系统中把对应的动态链接库加载到内存中。
对于gcc编译链接的时候动态库搜索顺序依次为:
- gcc编译时
-L
指定的路径去寻找 - 环境变量中的
LIBRARY_PATH
中指定的路径查找 /lib
,/usr/lib
和/usr/local/lib
下查找
执行二进制文件时动态库搜索顺序依次为和GCC编译的时候查找顺序一致
可以通过ldd命令查看一个可执行程序依赖的共享库
由于链接动态库和静态库的路径可能重合,所以如果在路径中有同名的静态库文件和动态库文件,比如libtest.a和libtest.so,gcc默认会优先选择动态链接库,会链接libtest.so,如果让gcc链接libtest.a,可以通过-static
,这样会强制选择静态库进行连接
使用动态链接库进行连接
$ gcc helloworld.c -o helloworld
$ size helloworld
text data bss dec hex filename
1141 492 16 1649 671 helloworld
$ ldd helloworld
linux-vdso.so.1 => (0x00007ffd66b05000)
libc.so.6 => /lib64/libc.so.6 (0x00007fd857a45000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd857dd1000)
使用静态链接库进行连接
$ gcc -static helloworld.c -o helloworld
$ size helloworld
text data bss dec hex filename
678451 5792 10464 694707 a99b3 helloworld
$ ldd helloworld
not a dynamic executable
链接器链接后生成的最终文件为ELF格式可执行文件,一个ELF可执行文件通常被链接为不同的段,常见的段譬如.text
、.data
、.rodata
和.bss
等段。
分析ELF文件
ELF文件的段
格式如图所示
位于ELF Header
和Section Header Table
之间的都是段
.text
: 已编译程序的指令段代码.rodata
:只读数据,例如常数const.data
:已初始化的C程序全局变量和静态局部变量.bss
:未出世的C程序全局变量和静态局部变量.debug
:调试符号表,调试器用此段的信息帮助调试
可以使用readelf -S
查看其各个section的信息如下
$ readelf -S helloworld
There are 33 section headers, starting at offset 0xa7ac8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400158 00000158
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.build-i NOTE 0000000000400178 00000178
0000000000000024 0000000000000000 A 0 0 4
[ 3] .rela.plt RELA 00000000004001a0 000001a0
0000000000000108 0000000000000018 A 0 5 8
[ 4] .init PROGBITS 00000000004002a8 000002a8
0000000000000018 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 00000000004002c0 000002c0
00000000000000b0 0000000000000000 AX 0 0 4
[ 6] .text PROGBITS 0000000000400380 00000380
000000000007dfc8 0000000000000000 AX 0 0 32
[ 7] __libc_freeres_fn PROGBITS 000000000047e350 0007e350
0000000000001a08 0000000000000000 AX 0 0 16
[ 8] __libc_thread_fre PROGBITS 000000000047fd60 0007fd60
00000000000000bf 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS 000000000047fe20 0007fe20
000000000000000e 0000000000000000 AX 0 0 4
[10] .rodata PROGBITS 000000000047fe40 0007fe40
00000000000183d7 0000000000000000 A 0 0 32
[11] __libc_atexit PROGBITS 0000000000498218 00098218
0000000000000008 0000000000000000 A 0 0 8
[12] __libc_subfreeres PROGBITS 0000000000498220 00098220
0000000000000060 0000000000000000 A 0 0 8
[13] __libc_thread_sub PROGBITS 0000000000498280 00098280
0000000000000008 0000000000000000 A 0 0 8
[14] .stapsdt.base PROGBITS 0000000000498288 00098288
0000000000000001 0000000000000000 A 0 0 1
[15] .eh_frame PROGBITS 0000000000498290 00098290
000000000000d824 0000000000000000 A 0 0 8
[16] .gcc_except_table PROGBITS 00000000004a5ab4 000a5ab4
0000000000000116 0000000000000000 A 0 0 1
[17] .tdata PROGBITS 00000000006a6000 000a6000
0000000000000020 0000000000000000 WAT 0 0 8
[18] .tbss NOBITS 00000000006a6020 000a6020
0000000000000030 0000000000000000 WAT 0 0 8
[19] .ctors PROGBITS 00000000006a6020 000a6020
0000000000000018 0000000000000000 WA 0 0 8
[20] .dtors PROGBITS 00000000006a6038 000a6038
0000000000000018 0000000000000000 WA 0 0 8
[21] .jcr PROGBITS 00000000006a6050 000a6050
0000000000000008 0000000000000000 WA 0 0 8
[22] .data.rel.ro PROGBITS 00000000006a6060 000a6060
0000000000000070 0000000000000000 WA 0 0 16
[23] .got PROGBITS 00000000006a60d0 000a60d0
0000000000000018 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 00000000006a60e8 000a60e8
0000000000000070 0000000000000008 WA 0 0 8
[25] .data PROGBITS 00000000006a6160 000a6160
0000000000001550 0000000000000000 WA 0 0 32
[26] .bss NOBITS 00000000006a76c0 000a76b0
0000000000002880 0000000000000000 WA 0 0 32
[27] __libc_freeres_pt NOBITS 00000000006a9f40 000a76b0
0000000000000030 0000000000000000 WA 0 0 8
[28] .note.stapsdt NOTE 0000000000000000 000a76b0
000000000000028c 0000000000000000 0 0 4
[29] .comment PROGBITS 0000000000000000 000a793c
000000000000002d 0000000000000001 MS 0 0 1
[30] .shstrtab STRTAB 0000000000000000 000a7969
000000000000015f 0000000000000000 0 0 1
[31] .symtab SYMTAB 0000000000000000 000a8308
000000000000b220 0000000000000018 32 766 8
[32] .strtab STRTAB 0000000000000000 000b3528
0000000000006c74 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
反汇编ELF
由于ELF文件无法被当做普通文本文件打开,如果希望直接查看一个ELF文件包含的指令和数据,需要使用反汇编的方法。
$ objdump -D helloworld
......省略部分
0000000000400494 <main>:
// PC地址: 指令编码 指令汇编格式
400494: 55 push %rbp
400495: 48 89 e5 mov %rsp,%rbp
400498: bf 50 fe 47 00 mov $0x47fe50,%edi
40049d: e8 fe 11 00 00 callq 4016a0 <_IO_puts>
4004a2: b8 00 00 00 00 mov $0x0,%eax
4004a7: c9 leaveq
4004a8: c3 retq
4004a9: 90 nop
4004aa: 90 nop
4004ab: 90 nop
4004ac: 90 nop
4004ad: 90 nop
4004ae: 90 nop
4004af: 90 nop
......省略部分
使用objdump -S
将其反汇编并且将其C语言源代码混合显示出来
$ gcc -o helloworld -g helloworld.c
$ objdump -S helloworld
00000000004004c4 <main>:
#include <stdio.h>
//此程序很简单,仅仅打印一个Hello World的字符串。
int main(void)
{
4004c4: 55 push %rbp
4004c5: 48 89 e5 mov %rsp,%rbp
printf("Hello World! \n");
4004c8: bf d8 05 40 00 mov $0x4005d8,%edi
4004cd: e8 e6 fe ff ff callq 4003b8 <puts@plt>
return 0;
4004d2: b8 00 00 00 00 mov $0x0,%eax
}
4004d7: c9 leaveq
4004d8: c3 retq
4004d9: 90 nop
4004da: 90 nop
4004db: 90 nop
4004dc: 90 nop
4004dd: 90 nop
4004de: 90 nop
4004df: 90 nop