Linux程序编译过程

时间:Sept. 30, 2018 分类:

目录:

介绍

转载

语言

计算机程序的语言分为机器语言,汇编语言和高级语言三类。

高级语言通过翻译成机器语言才能执行,翻译又分两种,一种是编译型,另一种是解释型。

编译型语言包括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编译链接的时候动态库搜索顺序依次为:

  1. gcc编译时-L指定的路径去寻找
  2. 环境变量中的LIBRARY_PATH中指定的路径查找
  3. /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 HeaderSection 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