Program-Compilation-and-ELF-Format-Lab-1

实验目的

1、熟悉 C 代码的编译过程与 ELF 文件格式。

2、初步学习使用 Linux 平台的二进制分析工具。

实验原理

C 代码的编译过程与 ELF 文件格式。

实验环境

Ubuntu 16.04 虚拟机。

初始设置

使用下述命令进行系统更新:

$ cd /home/binary && rm -f auto-update.sh 
&& wget -q --no-check-certificate 
https://practicalbinaryanalysis.com/patch/auto-update.sh 
&& chmod 755 auto-update.sh && ./auto-update.sh

进行系统更新

设置时间戳:

打开"显示隐藏文件"选项,打开.bashrc文件,在最后加上:

export PS1='\D{[%F %T]} ${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$'

加上语句

使用 source \sim/.bashrc 使其生效:

时间戳显示成功

哦对,还要改时区:

修改时区

实验任务

Task 1:C 编译过程

预处理阶段

进入到 /code/chapter1 目录下,输入 gcc -E -P compilation_example.c,输出结果为:

输出结果

...
int
main(int argc, char *argv[]) {
  printf("%s", "Hello, world!\n");
  return 0;
}

Q:对比原始代码,当前 main 函数有什么变化?

A:printf语句中的内容从参数(FORMAT_STRING ,MESSAGE)变成了这两个参数所指向的具体的内容("%s", "Hello,world!\setminusn")。

编译阶段

输入 gcc -S compilation_example.c:

输出结果

输出结果为:

    .file    "compilation_example.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
    subq    $16, %rsp
    movl    %edi, -4(%rbp)
    movq    %rsi, -16(%rbp)
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

汇编阶段

输入:gcc -c compilation_example.c:

输出结果

Q:当前生成的文件类型是什么?这个文件是否可以执行?为什么?

A:生成的文件是compilation_example.o,类型为目标文件;不可以直接执行,因为它还没有链接到其他的目标文件,所以它的代码段中有一些未定义的符号。

链接阶段

输入: gcc compilation_example.c, file a.out,./a.out

输出结果

Q:当前生成的文件类型是什么?

A:生成的文件是a.out,类型为可执行文件。

Task 2:符号与剥离的二进制文件

分别使用 nm 和 readelf 输出 a.out 二进制文件中的符号。

nm输出结果:

...
0000000000400526 T main
                 U puts@@GLIBC_2.2.5
00000000004004a0 t register_tm_clones
0000000000400430 T _start
0000000000601038 D __TMC_END__

nm输出结果

readelf中参数 -s 意为显示符号表,readelf输出结果:

...
    __libc_start_main@@GLIBC_
    54: 0000000000601028     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    55: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    56: 0000000000601030     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    57: 00000000004005d0     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
    58: 0000000000400550   101 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
    59: 0000000000601040     0 NOTYPE  GLOBAL DEFAULT   26 _end
    60: 0000000000400430    42 FUNC    GLOBAL DEFAULT   14 _start
    61: 0000000000601038     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    62: 0000000000400526    32 FUNC    GLOBAL DEFAULT   14 main
    63: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    64: 0000000000601038     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    65: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    66: 00000000004003c8     0 FUNC    GLOBAL DEFAULT   11 _init

readelf输出结果

Q:main 函数加载到内存时的驻留地址是什么?

A:驻留地址是0000000000400526。

--strip-all参数告诉strip移除文件中的所有符号信息。使用 strip 命令进行剥离,对于 a.out 再次执行 file 和 readelf -s:

输出结果

Q:此时对于 a.out 再次执行 file 和 readelf -s,结果有什么变化?运行 a.out,其功能有何变化?

A:使用file命令时发现出现了stripped状态;使用readlf -s命令时发现符号表".symtab"被全部删除。运行a.out,发现依然输出"Hello, world!",功能无变化。

Task 3:反汇编二进制文件

-sj .rodata参数告诉objdump命令仅显示'.rodata'(只读数据)段的内容。输入objdump -sj .rodata compilation_example.o:

输出结果

Q:有哪些只读数据存储在.rodata 段中?

A:.rodata 段内容:0000 48656c6c 6f2c2077 6f726c64 2100 Hello, world!. 即只有输出内容存储在.rodata 段中。

-h参数告诉readelf显示ELF文件头的信息。输入readelf -h compilation_example.o:

输出结果

-d 参数告诉objdump显示反汇编目标文件的所有代码段。输入objdump -d compilation_example.o:

输出结果

反汇编一个完整的可执行二进制文件 a.out:

1、gcc compilation_example.c 指令重新生成 a.out 文件。

2、objdump -d a.out 指令反汇编 a.out。

完整的a.out的输出结果

3、strip --strip-all a.out 指令剥离基本符号信息

4、objdump -d a.out 指令反汇编被剥离的 a.out。

被剥离的a.out的输出结果

Q:对比带符号的二进制文件和已剥离的二进制文件的反汇编结果,你有什么发现?(在.text 方面)

A:在.text段,两者的机器指令是相同的。这是因为剥离符号信息并不会改变程序的执行逻辑。但是变量名有所区别,

正常的a.out的.text输出结果

被剥离的a.out的.text输出结果

思考题

Q:你能识别 ELF 头部中字段代表的含义吗?尝试在 xxd 输出中找到所有 ELF 头部的字段,并解析这些字段内容的含义。

A:输入xxd compilation_example.o | head -n 30 查看前30行内容。我们可以按照 ELF 格式规范解析出各个字段的内容。以下是解析过程:

魔数(Magic):7f45 4c46:ELF 文件的魔数,表示这是一个 ELF 文件。

类别(Class):02:表示这是一个 64 位的 ELF 文件。

数据编码(Data):01:表示这是一个小端序(Little Endian)的 ELF 文件。

版本(Version):01:表示当前版本为 1。

操作系统/ABI(OS/ABI):00:表示 System V ABI。

ABI 版本(ABI Version):00:表示 ABI 版本为 0。

填充字节(Padding):0000 0000 0000 0000:7 个字节的填充,用于对齐。

文件类型(Type):0100:表示这是一个可重定位文件(Relocatable File)。

机器架构(Machine):3e00:表示这是一个 AMD x86-64 架构。

版本(Version):0100 0000:表示当前版本为 1。

入口点地址(Entry point address):0000 0000 0000 0000:表示入口点地址为 0。

程序头部表的文件偏移量(Start of program headers):0000 0000 0000 0000:表示程序头部表的偏移量为 0。

节头部表的文件偏移量(Start of section headers):c002 0000 0000 0000:表示节头部表的偏移量为 0x2c0。

标志(Flags):0000 0000:表示没有特殊标志。

头部大小(Size of this header):4000:表示 ELF 头部的大小为 64 字节。

程序头部表中每个条目的大小(Size of program headers):0000:表示程序头部表中每个条目的大小为 0。

程序头部表中条目的数量(Number of program headers):0000:表示程序头部表中没有条目。

节头部表中每个条目的大小(Size of section headers):4000:表示节头部表中每个条目的大小为 64 字节。

节头部表中条目的数量(Number of section headers):0d00:表示节头部表中有 13 个条目。

节头部字符串表在节头部表中的索引(Section header string table index):0a00:表示节头部字符串表在节头部表中的索引为 10。

之后的内容不属于 ELF 头部字段,而是程序的机器码和其他信息。

输出结果