从main函数启动过程说起

先贴代码:

#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的源码中也见过。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值