重学c语言(二、反汇编和hello_worldo分析)

之前的计划就是想重新学习一边编译链接的过程,这次是安排上了,但是还是感觉太年轻了,以上一篇的问题,去准备这一篇的时候,发现学习的东西还是很多的,应该是很多很多,惆怅。

但是flag已经发出去了,就硬着头皮去搞把,希望能坚持下来。加油。。。

2.1 目标文件

我们按照上一节的步骤,我们分步编译出来了,预处理文件.i,还有汇编文件.s,还有目标文件.o。

预处理文件上一篇就讲过了,汇编文件的话里面都是一些汇编语句,这个以后再讲,这一节我们的目标就是.o文件。

我有在之前的hello_world的基础上,添加了一些变量,和一个函数调用,这样分析起来会更全面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码#include <stdio.h>

int g_a = 0;
int g_b = 84;

int func1(int i)
{
printf("i = %d\n", i);
return 0;
}

int main(int argc, char **argv)
{
static int s_a = 0;
static int s_b = 84;

int a = 1;
int b;
func1(s_a+s_b+a+b);
printf("hello world %d %d %d\n", g_a, a, b);

return 0;
}

在这里hello_world中,我们定义了全局变量,局部变量,函数调用。

就按上一节的操作,我们直接汇编成.o文件。

我们可以使用file命令查看这个文件的格式:

1
2
shell复制代码root@ubuntu:~/c_test/02# file hello_world.o 
hello_world.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

很明显hello_world.o文件也是一个ELF文件,不过不是可执行文件,是可重定位的文件,等待链接器把多个.o文件链接起来,怎么链接呢?下节再讲。

2.2 objdump命令

linux下,已经准备了两个命令给我们,就是给我们这些不安分的,一个是objdump反汇编的命令,一个是readelf解析elf的命令,这样我们就不用一个字节一个字节去对比elf文件,如果有兴趣的话,也可以去一个字节一个字节对比,不过我这样就不对比了,后来的任务巨重。

下面简单介绍一个objdump常用的参数:

1
2
3
4
5
6
7
shell复制代码objdump -f hello_world.o		# 显示文件的头信息
objdump -h hello_world.o # 显示常用段信息
objdump -x hello_world.o # 显示全面的段信息
objdump -d hello_world.o # 显示机制指令段的汇编信息 很常用
objdump -D hello_world.o # 显示全部汇编信息
objdump -s hello_world.o # 显示请求的所有部分的全部内容,以十六进制的方式
objdump -t hello_world.o # 显示符号表,类似nm -s

2.3 readelf命令

接着看看readelf常用的参数:

1
2
3
4
5
shell复制代码readelf -h hello_world.o		# 显示elf格式的头信息
readelf -S hello_world.o # 显示elf段表
readelf -s hello_world.o # 显示符号表
readelf -r hello_world.o # 显示重定位
readelf -d hello_world.o # 显示动态段

其实也不是很熟这些命令,以后有常用在添加。

2.4 hello_world.o分析

来到了这一章的重点,我们先分析hello_world.o,下次分析hello_world,以后看安排要不要把.a .so的也分析一波。

2.4.1 头信息

查看头信息,两个命令都可以查看到:

2.4.1.1 objdump -f

1
2
3
4
5
6
shell复制代码root@ubuntu:~/c_test/02# objdump -f hello_world.o

hello_world.o: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000

可以查看到编译文件的系统架构,文件的格式,后面还有文件的标记:

1
2
3
4
5
6
7
8
arduino复制代码/* BFD contains relocation entries.  */
#define HAS_RELOC 0x01

/* BFD is directly executable. */
#define EXEC_P 0x02
...
/* BFD has symbols. */
#define HAS_SYMS 0x10

2.4.1.2 readelf -h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
shell复制代码root@ubuntu:~/c_test/02# readelf -h hello_world.o 
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1168 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 10
root@ubuntu:~/c_test/02#

这个readelf命令重点是根据elf的二进制编码,然后把属于头部分的信息,转码出来,所以我们看的就是这个样子,又回想起当年,动不动就分析二进制的时候,哎,现在有一个工具多爽,直接输入一个命令就出来了,就不用去对这二进制数据分析了。

2.4.2 段信息

段信息是一个很重要的知识点,段信息也是可以使用两个命令来读取的。

这里readelf -S这个我们留着下节去分析hello_world,这一节我们用objdump -h。

先来总览一下:

1
2
3
shell复制代码root@ubuntu:~/c_test/02# size hello_world.o
text data bss dec hex filename
245 8 8 261 105 hello_world.o

我们可以用size来简单查看各个段大小,text段、data段、bss段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码root@ubuntu:~/c_test/02# objdump -h hello_world.o

hello_world.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000007f 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 000000c0 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000008 0000000000000000 0000000000000000 000000c8 2**2
ALLOC
3 .rodata 0000001e 0000000000000000 0000000000000000 000000c8 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000036 0000000000000000 0000000000000000 000000e6 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000011c 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 00000120 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

重点出来了,接下来的目标,我们就重点分析这几个段。

2.4.2.1 text段

这一段是专门存放代码的,我们就来看看代码最后是怎么存的?想看不?

看的话还是需要objdump这个老工具人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码root@ubuntu:~/c_test/02# objdump -s hello_world.o 

hello_world.o: file format elf64-x86-64

Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 bf000000 00b80000 0000e800 000000b8 ................
0020 00000000 c9c35548 89e54883 ec20897d ......UH..H.. .}
0030 ec488975 e0c745f8 01000000 8b150000 .H.u..E.........
0040 00008b05 00000000 01c28b45 f801c28b ...........E....
0050 45fc01d0 89c7e800 0000008b 05000000 E...............
0060 008b4dfc 8b55f889 c6bf0000 0000b800 ..M..U..........
0070 000000e8 00000000 b8000000 00c9c3 ...............
... 只留下 .text 段
root@ubuntu:~/c_test/02#

-s 是把所有段以十六进制的格式输出,这也是代码中存储的格式,直接看十六进制看不懂,是不是,所以我们还需要把机器指令段反汇编出来,一对比就明白了很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
shell复制代码root@ubuntu:~/c_test/02# objdump -d hello_world.o 

hello_world.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <func1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: bf 00 00 00 00 mov $0x0,%edi
15: b8 00 00 00 00 mov $0x0,%eax
1a: e8 00 00 00 00 callq 1f <func1+0x1f>
1f: b8 00 00 00 00 mov $0x0,%eax
24: c9 leaveq
25: c3 retq

0000000000000026 <main>:
26: 55 push %rbp
27: 48 89 e5 mov %rsp,%rbp
2a: 48 83 ec 20 sub $0x20,%rsp
2e: 89 7d ec mov %edi,-0x14(%rbp)
31: 48 89 75 e0 mov %rsi,-0x20(%rbp)
35: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
3c: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 42 <main+0x1c>
42: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 48 <main+0x22>
48: 01 c2 add %eax,%edx
4a: 8b 45 f8 mov -0x8(%rbp),%eax
4d: 01 c2 add %eax,%edx
4f: 8b 45 fc mov -0x4(%rbp),%eax
52: 01 d0 add %edx,%eax
54: 89 c7 mov %eax,%edi
56: e8 00 00 00 00 callq 5b <main+0x35>
5b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 61 <main+0x3b>
61: 8b 4d fc mov -0x4(%rbp),%ecx
64: 8b 55 f8 mov -0x8(%rbp),%edx
67: 89 c6 mov %eax,%esi
69: bf 00 00 00 00 mov $0x0,%edi
6e: b8 00 00 00 00 mov $0x0,%eax
73: e8 00 00 00 00 callq 78 <main+0x52>
78: b8 00 00 00 00 mov $0x0,%eax
7d: c9 leaveq
7e: c3 retq

左侧第一列,是这条语句的偏移,通过这个就能明白x86的机器命令是变长的。

左侧第二列,就是十六进制的内容,可以看成机器码。

右侧,就是对应的汇编指令了。

通过我们对比两个的十六进制,是不是完全符合,所以说代码存储在文件中,就是这样存储的。

2.4.2.2 .data段

程序中初始化的数据就是不为0的全局变量,不为0的局部静态变量

我们看上面的这个段是8个字节,怎么是8个字节呢?

在这里插入图片描述

这两个数据是存储在data段中的,不信是不是?

我们用反汇编来验证一波:

1
2
shell复制代码Contents of section .data:
0000 54000000 54000000 T...T...

0x54000000 = 84,这里面就是存了两个84。

为什么最低位在前面,这个就是linux系统一般都是小端模式。

顺序的话,应该是按变量的加载顺序,取值。

2.4.2.3 BSS段

程序中为0的全局变量,为0的静态变量,或者是不赋值,不赋值也会默认为0

为什么会分出一个.bss段,是因为这样可以节省一点空间,等程序运行的时候在搞上来,所以这个段是需要大小的,统计程序中符合这个段的变量大小,留下空间,但是因为值为0,所以不要存储。

CONTENTS表示是这个.o程序包含CONTENTS这个字段,因为bss没有,所以没包含。

2.4.2.4 rodata

只读数据段,一般是放的是常量(const),比如字符串常量。

为什么会出现这样的只读数据段,这是在编译器中把这些只读的数据存储到一起,如果这段数据被修改,就会出现报错,编译器是这样检测只读数据的。

不过好像可以用一个指针指向这个地址,然后就可以直接修改了,因为linux系统,存放只读数据也是放在RAM上,理论上也是可以绕过编译器修改的。

像51单片机,因为RAM太少,只读数据是直接存储到flash中的,flash是一个不能改的存储介质,这个用指针指向也是不能修改的。

其实我很疑惑的一定就是这个代码中没有只读数据啊,怎么会有呢?

这就是我们c语言功底不够用,其实printf函数中的字符串,就是存储成字符串常量的,就是存在这个只读数据段的。不信?我们反汇编看看:

1
2
3
shell复制代码Contents of section .rodata:
0000 69203d20 25640a00 68656c6c 6f20776f i = %d..hello wo
0010 726c6420 25642025 64202564 0a00 rld %d %d %d..

是不是安排的明明白白,清清楚楚的。

2.4.2.5 comment

包含编译器的信息段

1
2
3
4
5
shell复制代码Contents of section .comment: 
0000 00474343 3a202855 62756e74 7520352e .GCC: (Ubuntu 5.
0010 342e302d 36756275 6e747531 7e31362e 4.0-6ubuntu1~16.
0020 30342e31 32292035 2e342e30 20323031 04.12) 5.4.0 201
0030 36303630 3900 60609.

2.4.2.6 .note.GNU-stack

堆栈提示段,表示不是很明白,

2.4.2.7 .eh_frame

保存c++异常处理的内容

1
2
3
4
5
6
7
shell复制代码Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 26000000 00410e10 8602430d ....&....A....C.
0030 06610c07 08000000 1c000000 3c000000 .a..........<...
0040 00000000 59000000 00410e10 8602430d ....Y....A....C.
0050 0602540c 07080000 ..T.....

这一段也不懂,以后再补补,不懂的地方还有很多。

2.4.2.8 自定义段

1
2
3
4
5
6
7
shell复制代码__attribute__((section("FOO"))) int global = 42;

__attribute__((section("BAR"))) void foo()

{undefined

}

当初分析uboot代码的时候,就很多这种自定义段,uboot启动后,会把同一个初始化的函数放到同一个段,然后好像是依次编译这个段的内容。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%