前一段时间对Linux不熟,所以很多命令不知道。学习C时候需要偶尔看一下汇编用来理解。我喜欢用问题的形式来学习和总结。
1. 如何看一个程序代码变量的存储布局?
这个问题在查看C代码中的一些关键字的作用很有效。如:const、static、extern等。readelf这个工具就派上用场了。
(1)readelf工具
a. 作用:用来显示ELF文件的信息。
b. 使用:readelf <option> <file>,其中我们常用选项有-a,<file>可以是目标文件或可执行文件。
(想了解该工具详细的信息,请通过Linux下的man命令查一下。)
下面通过例子来使用一下这个工具:
- int main(void)
- {
- printf("hello, welcome!\n");
- return 0;
- }
- gcc -g hello.c -o hello
- readelf -a hello
- 我们会看到:
- LF Header:
- Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
- Class: ELF32
- Data: 2's complement, little endian
- Version: 1 (current)
- OS/ABI: UNIX - System V
- ABI Version: 0
- Type: EXEC (Executable file)
- Machine: Intel 80386
- Version: 0x1
- Entry point address: 0x8048330
- Start of program headers: 52 (bytes into file)
- Start of section headers: 5052 (bytes into file)
- Flags: 0x0
- Size of this header: 52 (bytes)
- Size of program headers: 32 (bytes)
- Number of program headers: 8
- Size of section headers: 40 (bytes)
- Number of section headers: 38
- Section header string table index: 35
从文件地址5052开始,每个Section Header 占40字节,共320 字节,到文件地址0x207 结束。文件地址是这样定义的:
文件开头第一个字节的地址是0x8048330。
- Section Headers:
- [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
- [ 0] NULL 00000000 000000 000000 00 0 0 0
- ...
- [10] .rel.dyn REL 08048298 000298 000008 08 A 6 0 4
- [11] .rel.plt REL 080482a0 0002a0 000018 08 A 6 13 4
- [12] .init PROGBITS 080482b8 0002b8 000030 00 AX 0 0 4
- [13] .plt PROGBITS 080482e8 0002e8 000040 04 AX 0 0 4
- [14] .text PROGBITS 08048330 000330 00016c 00 AX 0 0 16
- [15] .fini PROGBITS 0804849c 00049c 00001c 00 AX 0 0 4
- [16] .rodata PROGBITS 080484b8 0004b8 000018 00 A 0 0 4
- ...
- [22] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4
- [23] .got.plt PROGBITS 08049ff4 000ff4 000018 04 WA 0 0 4
- [24] .data PROGBITS 0804a00c 00100c 000008 00 WA 0 0 4
- [25] .bss NOBITS 0804a014 001014 000008 00 WA 0 0 4
- [26] .comment PROGBITS 00000000 001014 000023 01 MS 0 0 1
- [27] .debug_aranges PROGBITS 00000000 001037 000020 00 0 0 1
- ...
- [35] .shstrtab STRTAB 00000000 001264 000156 00 0 0 1
- [36] .symtab SYMTAB 00000000 0019ac 000490 10 37 53 4
- [37] .strtab STRTAB 00000000 001e3c 0001fb 00 0 0 1
从Section Header 中读出各Section的描述信息,这里有38个SectionHeader。其中,我们关心的有.text,.rodata,.data,.bss。
它们分别是代码段,常量区,数据段,未初始化数据段。Addr是这些段加载到内存中的地址(都是虚拟地址),加载地址要在链接时填写,
现在空缺,所以是全0 。Off 和Size两列指出了各Section的文件地址,比如.data段从文件地址00100c开始,一共0x08个字节,根据以上信息可以描绘出
整个文件的布局。
(2)查看一个程序代码变量的存储布局
先看例子:
- #include <stdio.h>
- const int g_A = 10;
- int a = 20;
- static int g_C = 30;
- int g_D;
- int main(void)
- {
- static int a = 40;
- register int b = 50;
- printf("Welcome%d\n", b);
- return 0;
- }
- 用gcc编译后,通过readelf工具查看可执行文件。
- gcc -g var.c -o var
- readelf -a var
- 截取我们所关心的几行:
- Section Headers:
- [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
- ...
- [14] .text PROGBITS 08048390 000390 0001bc 00 AX 0 0 16
- ...
- [16] .rodata PROGBITS 08048568 000568 00001c 00 A 0 0 4
- ...
- [24] .data PROGBITS 0804a010 001010 000014 00 WA 0 0 4
- [25] .bss NOBITS 0804a024 001024 00000c 00 WA 0 0 4
- Symbol table '.symtab' contains 79 entries:
- Num: Value Size Type Bind Vis Ndx Name
- ...
- 49: 0804a01c 4 OBJECT LOCAL DEFAULT 24 g_C
- 50: 0804a020 4 OBJECT LOCAL DEFAULT 24 a.1706
- 60: 0804a02c 4 OBJECT GLOBAL DEFAULT 25 g_D
- 66: 0804a018 4 OBJECT GLOBAL DEFAULT 24 a
- 75: 08048570 4 OBJECT GLOBAL DEFAULT 16 g_A
g_A:
变量g_A用const修饰,表示g_A是只读的,不可修改,它被分配的地址是0x08048570 ,从readelf的输出的Section Headers可以看到这个地址位于.rodata段。
它在文件中的地址是0x568~0x584. 我们也可以通过hexdump命令查看这块内存的内容:hexdump -C var
00000570 0a 00 00 00 57 65 6c 63 6f 6d 65 20 25 64 0a 00 |....Welcome %d..|
我们看到,0x570地址存的就是0a 00 00 00。也即十进制的10.我们还看到程序中的字符串字面值"Hello world!\n"分配在.rodata段的末尾。
字符串字面值是只读的,相当于在全局作用域定义了一个const数组:
const char helloworld[] = {'W', 'e', 'l', 'c', 'o', 'm', 'e', '%', 'd', '\n', '\0'};
程序加载运行时,.rodata段和.text段通常合并到一个Segment中,操作系统将这个Segment的页面只读保护起来,防止意外的改写。
注意,像A 这种const变量在定义时必须初始化。因为只有初始化时才有机会给它一个值,一旦定义之后就不能再改写了,也就是不能再赋值了。
从上面readelf的输出可以看到.data段从地址0x0804a010开始,长度是0x14,也就是到地址0x804a024 结束。在.data段中有三个变量a ,g_C和a.1706。
a:
a是一个GLOBAL的符号,而g_C被static关键字修饰了,导致它成为一个LOCAL的符号,所以static 在这里的作用是声明b这个符号为LOCAL的,不被链接器处理。
还有一个a.1706是什么呢?它就是main函数中的static int a。函数中的static变量不同于局部变量,它并不是在调用函数时分配,在函数返回时释放,
而是像全局变量一样静态分配,所以用“ static” (静态)这个词。另一方面,函数中的static 变量的作用域和以前讲的局部变量一样,只在函数中起作用,比如main函数中的a 这个变量名只在main函数中起作用,在别的函数中
说变量a 就不是指它了,所以编译器给它的符号名加了一个后缀,变成a.1589 ,以便和全局变量a 以及其它函数的变量a 区分开。
g_D:
.bss段从地址0x804a024开始(紧挨着.data段),长度为0xc,也就是到地址0x804a030结束。变量g_D位于这个段。因为g_D未初始化。
局部变量c和数组b在下一个知识点说明。
变量c 并没有在栈上分配存储空间,而是直接存在eax寄存器里,后面调用printf也是直接从eax寄
存器里取出c 的值当参数压栈,这就是register 关键字的作用,指示编译器尽可能分配一个寄存器
来存储这个变量。我们还看到调用printf 时对于"Hello world %d\n" 这个参数压栈的是它
在.rodata段中的首地址,而不是把整个字符串压栈,所以在第 4 节 “ 字符串” 中说过,字符串在使
用时可以看作数组名,如果做右值则表示数组首元素的地址(或者说指向数组首元素的指针),我
们以后讲指针还要继续讨论这个问题。
2. 如何反汇编?
我们在理解某些C语言代码时,有时候要深入理解,就必须跑到汇编去理解。所以反汇编也很重要,使用的命令:objdump -dS <file>
我们给上面的代码加一个数组b,修改后的代码如下:
- #include <stdio.h>
- const int g_A = 10;
- int a = 20;
- static int g_C = 30;
- int g_D;
- int main(void)
- {
- static int a = 40;
- register int c = 50;
- char b[] = "Hello world";
- printf("Welcome%d\n", c);
- return 0;
- }
- 我们通过objdump -dS var查看程序对应的汇编代码:
- 08048444 <main>:
- int a = 20;
- static int g_C = 30;
- int g_D;
- int main(void)
- {
- 8048444: 55 push %ebp
- 8048445: 89 e5 mov %esp,%ebp
- 8048447: 83 e4 f0 and $0xfffffff0,%esp
- 804844a: 53 push %ebx
- 804844b: 83 ec 2c sub $0x2c,%esp
- 804844e: 65 a1 14 00 00 00 mov %gs:0x14,%eax
- 8048454: 89 44 24 1c mov %eax,0x1c(%esp)
- 8048458: 31 c0 xor %eax,%eax
- static int a = 40;
- char b[] = "Hello World";
- 804845a: c7 44 24 10 48 65 6c movl $0x6c6c6548,0x10(%esp)
- 8048461: 6c
- 8048462: c7 44 24 14 6f 20 57 movl $0x6f57206f,0x14(%esp)
- 8048469: 6f
- 804846a: c7 44 24 18 72 6c 64 movl $0x646c72,0x18(%esp)
- 8048471: 00
- register int c = 50;
- 8048472: bb 32 00 00 00 mov $0x32,%ebx
- printf("Welcome %d\n", c);
- 8048477: b8 74 85 04 08 mov $0x8048574,%eax
- 804847c: 89 5c 24 04 mov %ebx,0x4(%esp)
- 8048480: 89 04 24 mov %eax,(%esp)
- 8048483: e8 dc fe ff ff call 8048364 <printf@plt>
- return 0;
- 8048488: b8 00 00 00 00 mov $0x0,%eax
- }
通过三条movl指令把12个字节写到栈上,这就是b 的存储空间。
地址的顺序依次是b[0]、b[1]、b[2] ……
数组元素b[n]的地址 = 数组的基地址(b 做右值就表示这个基地址) + n × 每个元素的字节数
当n=0 时,元素b[0]的地址就是数组的基地址,因此数组下标要从0 开始而不是从1 开始。
对于上例中的变量c,其汇编代码如下:
register int c = 50;
8048472: bb 32 00 00 00 mov $0x32,%ebx
变量c 并没有在栈上分配存储空间,而是直接存在ebx 寄存器里,后面调用printf 也是直接从ebx寄
存器里取出c 的值当参数压栈,这就是register关键字的作用,指示编译器尽可能分配一个寄存器来存储这个变量。