先贴代码:
#include <stdio.h>
const int A=10;
static int b=30;
static char msg[]="hello";
int c;
static int add(int a,int b)
{
return a+b;
}
void printmsg1(int a,int b)
{
int d= add(a,b);
printf("%d\n",d);
}
void printmsg2()
{
printf("%s\n",msg);
}
int main(int argc,void* argv[])
{
static int a=40;
register int c=50;
printmsg1(a,c);
printmsg2();
printf("hello");
return 0;
}
变量的存储结构
使用ReadElf -a main可以查看全局变量和局部变量是如何在内存中存储布局
49: 0804a018 4 OBJECT LOCAL DEFAULT 24 b
50: 0804a01c 6 OBJECT LOCAL DEFAULT 24 msg
52: 0804a024 4 OBJECT LOCAL DEFAULT 24 a.1724
72: 08048580 4 OBJECT GLOBAL DEFAULT 15 A
76: 0804a030 4 OBJECT GLOBAL DEFAULT 25 c
从中可以看出b、msg、a.1724存放在section 24,A存放在Section 15,c存放在Section 25。分别对应:.data、.bss、.rodata。
上面的LOCAL代表变量被static修饰,不会被链接器处理。GLOBAL代表变量没有被static修饰,会被链接器处理。
a.1724表示main函数中的a变量,由于它被static修饰因此不像局部变量在函数调用时分配内存函数退出时释放内存,而是一个全局变量只不过不能被链接器处理,且添加了后缀以区分之前的全局变量a。
readelf -l main输出:
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .ctors .dtors .jcr .dynamic .got
可以看到.rodata和.text存放在一个Segments中,可以被保护成只读。
.data和.bss存放在另外一个Segments中
对于函数中的局部变量,则使用栈结构进行内存分配和释放。
objdump -dS main.o输出反汇编
static int a=40;
register int c=50;
8048471: bb 32 00 00 00 mov $0x32,%ebx
printmsg1(a,c);
8048476: a1 24 a0 04 08 mov 0x804a024,%eax
804847b: 89 5c 24 04 mov %ebx,0x4(%esp)
804847f: 89 04 24 mov %eax,(%esp)
8048482: e8 9b ff ff ff call 8048422 <printmsg1>
和栈相关的寄存器有两个esp和ebp,分别标示栈顶和栈基。假设原来的esp的值为NN的话
调用printmsg1之前,先将c和a入栈,参数入栈的顺序是从右到左。call指令执行完成时,c保存在NN+4,a保存在NN;esp指向NN-4(这是因为call指令压入返回地址,上图应该是8048486)
再来看看printmsg1的反汇编
08048422 <printmsg1>:
void printmsg1(int a,int b)
{
8048422: 55 push %ebp
8048423: 89 e5 mov %esp,%ebp
8048425: 83 ec 28 sub $0x28,%esp
int d= add(a,b);
8048428: 8b 45 0c mov 0xc(%ebp),%eax
804842b: 89 44 24 04 mov %eax,0x4(%esp)
804842f: 8b 45 08 mov 0x8(%ebp),%eax
8048432: 89 04 24 mov %eax,(%esp)
8048435: e8 da ff ff ff call 8048414 <add>
804843a: 89 45 f4 mov %eax,-0xc(%ebp)
printf("%d\n",d);
804843d: b8 84 85 04 08 mov $0x8048584,%eax
8048442: 8b 55 f4 mov -0xc(%ebp),%edx
8048445: 89 54 24 04 mov %edx,0x4(%esp)
8048449: 89 04 24 mov %eax,(%esp)
804844c: e8 cf fe ff ff call 8048320 <printf@plt>
}
首先将ebp栈基保存到NN-8,将ebp重新指向NN-8,esp更新为NN-8-0x28。这样ebp和esp分别指向新的栈基和栈顶。
在调用函数之前,总是这样的:
- 将最右边的参数先压栈,左边的参数后压栈
- 进入调用函数后,再将调用函数的栈帧顶部压栈,同时改变当前的栈帧(通过修改ebp)
main文件链接过程
使用readelf -s main.o可以查看符号是否定义:
htm@htm:~/test/testassemble$ readelf -s main.o
Symbol table '.symtab' contains 19 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS main.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000000 4 OBJECT LOCAL DEFAULT 3 b
7: 00000004 6 OBJECT LOCAL DEFAULT 3 msg
8: 00000000 14 FUNC LOCAL DEFAULT 1 add
9: 0000000c 4 OBJECT LOCAL DEFAULT 3 a.1724
10: 00000000 0 SECTION LOCAL DEFAULT 7
11: 00000000 0 SECTION LOCAL DEFAULT 6
12: 00000000 4 OBJECT GLOBAL DEFAULT 5 A
13: 00000004 4 OBJECT GLOBAL DEFAULT COM c
14: 0000000e 49 FUNC GLOBAL DEFAULT 1 printmsg1
15: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 0000003f 20 FUNC GLOBAL DEFAULT 1 printmsg2
17: 00000000 0 NOTYPE GLOBAL DEFAULT UND puts
18: 00000053 63 FUNC GLOBAL DEFAULT 1 main
上面可以看到printf和puts都没有定义,在main.c中我们也找不到这两个函数的定义,肯定是在其他地方定义的。不过在编译过程,printf和puts是如何链接过来的?并且链接的是哪个文件?
htm@htm:~/test/testassemble$ ld main.o -o main
ld: warning: cannot find entry symbol _start; defaulting to 0000000008048094
main.o: In function `printmsg1':
main.c:(.text+0x39): undefined reference to `printf'
main.o: In function `printmsg2':
main.c:(.text+0x4d): undefined reference to `puts'
main.o: In function `main':
main.c:(.text+0x81): undefined reference to `printf'
可以猜想,肯定少了某些链接文件,编译的最后阶段,gcc应该是自动添加了一些链接文件。
htm@htm:~/test/testassemble$ gcc -v main.o -o main
Using built-in specs.
Target: i686-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.4.7-1ubuntu2' --with-bugurl=file:///usr/share/doc/gcc-4.4/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.4 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.4 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc --enable-targets=all --disable-werror --with-arch-32=i686 --with-tune=generic --enable-checking=release --build=i686-linux-gnu --host=i686-linux-gnu --target=i686-linux-gnu
Thread model: posix
gcc version 4.4.7 (Ubuntu/Linaro 4.4.7-1ubuntu2)
COMPILER_PATH=/usr/lib/gcc/i686-linux-gnu/4.4.7/:/usr/lib/gcc/i686-linux-gnu/4.4.7/:/usr/lib/gcc/i686-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/4.4.7/:/usr/lib/gcc/i686-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/4.4.7/:/usr/lib/gcc/i686-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/i686-linux-gnu/4.4.7/:/usr/lib/gcc/i686-linux-gnu/4.4.7/:/usr/lib/gcc/i686-linux-gnu/4.4.7/../../../i386-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/4.4.7/../../../../lib/:/lib/i386-linux-gnu/:/lib/../lib/:/usr/lib/i386-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/i686-linux-gnu/4.4.7/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'main' '-mtune=generic' '-march=i686'
/usr/lib/gcc/i686-linux-gnu/4.4.7/collect2 --build-id --eh-frame-hdr -m elf_i386 --hash-style=gnu -dynamic-linker /lib/ld-linux.so.2 -o main -z relro /usr/lib/gcc/i686-linux-gnu/4.4.7/../../../i386-linux-gnu/crt1.o /usr/lib/gcc/i686-linux-gnu/4.4.7/../../../i386-linux-gnu/crti.o /usr/lib/gcc/i686-linux-gnu/4.4.7/crtbegin.o -L/usr/lib/gcc/i686-linux-gnu/4.4.7 -L/usr/lib/gcc/i686-linux-gnu/4.4.7 -L/usr/lib/gcc/i686-linux-gnu/4.4.7/../../../i386-linux-gnu -L/usr/lib/gcc/i686-linux-gnu/4.4.7/../../../../lib -L/lib/i386-linux-gnu -L/lib/../lib -L/usr/lib/i386-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/i686-linux-gnu/4.4.7/../../.. main.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-linux-gnu/4.4.7/crtend.o /usr/lib/gcc/i686-linux-gnu/4.4.7/../../../i386-linux-gnu/crtn.o
从上面可以看出,链接过程其实自动包含了其他文件:crt1.o crti.o crtbeginT.ocrtend.o crtn.o等。
上图中printf在编译器中优化成puts(这样数据就能直接发送出去而不是先放在缓冲区)。puts被保存在libc中,本例使用的动态链接库,可以在库文件中找到。
那么main文件是如何执行的?
一般说来,不同的对象文件链接成为一个可执行文件的过程:将各个对象文件中不同的section进行合并,同时将对象文件中的符号所代表的地址进行重新赋值。例如mian.o中的printmsg2符号地址值为0x3f,而在main文件中地址被修改为0x8048453。
readelf -s main.o | grep printmsg2
readelf -s main | grep printmsg2
调用printmsg时,在main.o对象文件中,由于不知道printmsg2的加载地址,所以随便写了一个地址call 74 <main+0x21>
这个无效地址在连接过程中被改变为 call 8048453 <printmsg2>
链接器如何知道call 74就代表着调用printmsg2?
答案在main.o的.rel.text字段中,在这里定义了所有需要重定向的符号,连接器从这里找到哪些需要重新定义加载地址的符号
Relocation section '.rel.text' at offset 0xcf0 contains 9 entries:
Offset Info Type Sym.Value Sym. Name
0000002a 00000801 R_386_32 00000000 .rodata
00000039 00001702 R_386_PC32 00000000 printf
00000048 00000301 R_386_32 00000000 .data
0000004d 00001902 R_386_PC32 00000000 puts
00000063 00000301 R_386_32 00000000 .data
0000006f 00001602 R_386_PC32 0000000e printmsg1
00000074 00001802 R_386_PC32 0000003f printmsg2
00000079 00000801 R_386_32 00000000 .rodata
00000081 00001702 R_386_PC32 00000000 printf
上述文件中offset表示对象文件中偏移多少字节需要替换。例如printf的地址在main.o中的第0x81个字节偏移。
可执行文件的起点
前面说一般_start代表着程序执行的起点,这是因为在链接过程使用了默认的链接脚本中定义的。
htm@htm:~/test/testassemble$ ld -verbose
GNU ld (GNU Binutils for Ubuntu) 2.22
using internal linker script:
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
OUTPUT_FORMAT("elf32-i386", "elf32-i386",
"elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SEARCH_DIR("/usr/i686-linux-gnu/lib32"); SEARCH_DIR("=/usr/local/lib32"); SEARCH_DIR("=/lib32"); SEARCH_DIR("=/usr/lib32"); SEARCH_DIR("=/usr/local/lib/i386-linux-gnu"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib/i386-linux-gnu"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib/i386-linux-gnu"); SEARCH_DIR("=/usr/lib");
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x08048000)); . = SEGMENT_START("text-segment", 0x08048000) + SIZEOF_HEADERS;
.interp : { *(.interp) }
.note.gnu.build-id : { *(.note.gnu.build-id) }
.hash : { *(.hash) }
.gnu.hash : { *(.gnu.hash) }
.dynsym : { *(.dynsym) }
.dynstr : { *(.dynstr) }
.gnu.version : { *(.gnu.version) }
.gnu.version_d : { *(.gnu.version_d) }
.gnu.version_r : { *(.gnu.version_r) }
.rel.dyn :
{
*(.rel.init)
*(.rel.text .rel.text.* .rel.gnu.linkonce.t.*)
*(.rel.fini)
*(.rel.rodata .rel.rodata.* .rel.gnu.linkonce.r.*)
*(.rel.data.rel.ro* .rel.gnu.linkonce.d.rel.ro.*)
*(.rel.data .rel.data.* .rel.gnu.linkonce.d.*)
*(.rel.tdata .rel.tdata.* .rel.gnu.linkonce.td.*)
*(.rel.tbss .rel.tbss.* .rel.gnu.linkonce.tb.*)
*(.rel.ctors)
*(.rel.dtors)
*(.rel.got)
*(.rel.bss .rel.bss.* .rel.gnu.linkonce.b.*)
*(.rel.ifunc)
}
.rel.plt :
{
*(.rel.plt)
PROVIDE_HIDDEN (__rel_iplt_start = .);
*(.rel.iplt)
PROVIDE_HIDDEN (__rel_iplt_end = .);
}
.init :
{
KEEP (*(.init))
} =0x90909090
.plt : { *(.plt) *(.iplt) }
.text :
{
*(.text.unlikely .text.*_unlikely)
*(.text.exit .text.exit.*)
*(.text.startup .text.startup.*)
ENTRY(_start)指定了_start为程序的起点,这个起点也可以被修改。
上面的链接脚本文件在UBoot的源码中也见过。