2010-05-06 67 views
7

我写了一个简单的Hello World程序。对可执行文件和可重定位目标文件的疑问

#include <stdio.h> 
    int main() { 
    printf("Hello World"); 
    return 0; 
    } 

我想了解可重定位目标文件和可执行文件的外观。 对应主函数的对象文件是

0000000000000000 <main>: 
    0: 55      push %rbp 
    1: 48 89 e5    mov %rsp,%rbp 
    4: bf 00 00 00 00   mov $0x0,%edi 
    9: b8 00 00 00 00   mov $0x0,%eax 
    e: e8 00 00 00 00   callq 13 <main+0x13> 
    13: b8 00 00 00 00   mov $0x0,%eax 
    18: c9      leaveq 
    19: c3      retq 

这里的函数调用的printf是callq 13.有一件事我不明白的是为什么是13.这意味着调用函数在adresss 13,右??。 13有下一条指令,对吗?请解释一下这是什么意思?

对应于主可执行代码是

00000000004004cc <main>: 
    4004cc:  55      push %rbp 
    4004cd:  48 89 e5    mov %rsp,%rbp 
    4004d0:  bf dc 05 40 00   mov $0x4005dc,%edi 
    4004d5:  b8 00 00 00 00   mov $0x0,%eax 
    4004da:  e8 e1 fe ff ff   callq 4003c0 <[email protected]> 
    4004df:  b8 00 00 00 00   mov $0x0,%eax 
    4004e4:  c9      leaveq 
    4004e5:  c3      retq 

这callq 4003c0。但二进制指令是e8 e1 fe ff ff。没有什么与4003c0相对应。那我错了什么?

谢谢。 Bala

回答

7

在第一种情况下,查看指令编码 - 函数地址所在的位置都是零。那是因为对象还没有被链接,所以外部符号的地址还没有被连接。当您执行到可执行文件格式的最后一个链接时,系统会在其中粘贴另一个占位符,然后动态链接程序将在运行时最终为printf()添加正确的地址。这是我写的一个“你好,世界”程序的一个简单例子。

首先,目标文件的反汇编:

00000000 <_main>: 
    0: 8d 4c 24 04    lea 0x4(%esp),%ecx 
    4: 83 e4 f0    and $0xfffffff0,%esp 
    7: ff 71 fc    pushl -0x4(%ecx) 
    a: 55      push %ebp 
    b: 89 e5     mov %esp,%ebp 
    d: 51      push %ecx 
    e: 83 ec 04    sub $0x4,%esp 
    11: e8 00 00 00 00   call 16 <_main+0x16> 
    16: c7 04 24 00 00 00 00 movl $0x0,(%esp) 
    1d: e8 00 00 00 00   call 22 <_main+0x22> 
    22: b8 00 00 00 00   mov $0x0,%eax 
    27: 83 c4 04    add $0x4,%esp 
    2a: 59      pop %ecx 
    2b: 5d      pop %ebp 
    2c: 8d 61 fc    lea -0x4(%ecx),%esp 
    2f: c3      ret  

然后重新定位:

main.o:  file format pe-i386 

RELOCATION RECORDS FOR [.text]: 
OFFSET TYPE    VALUE 
00000012 DISP32   ___main 
00000019 dir32    .rdata 
0000001e DISP32   _puts 

正如你可以看到有一个搬迁那里_puts,这是调用printf开启成。该重新定位将在链接时被注意到并被修复。在动态库链接的情况下,在程序运行之前,重定位和修正可能无法完全解决,但我希望从这个例子中可以得到这个想法。

+0

来自downvoter的评论? – 2010-05-06 23:24:00

5

调用在x86中是相对的,如果你有e8,则调用位置是addr + 5。

e1 fe ff ff a是小端编码的相对跳转。这实际上意味着fffffee1

现在添加这个到呼叫指令的地址+ 5: (0xfffffee1 + 0x4004da + 5) % 2**32 = 0x4003c0

+1

+5是因为它在调用后相对于* next *指令,并且该调用长度为5个字节。 – caf 2010-05-06 23:01:53

+0

x86上的调用可以是相对的也可以是绝对的。只是'E8'是一个相对的呼叫。 – AnT 2010-05-06 23:26:45

+0

是的,我忘记了也有绝对的目的地,但它们要么由segment:selector指定,要么指向要跳转到的地址的指针。 – 2010-05-07 00:46:10

7

E8指令呼叫(call)被指定为相对的目标偏移量从当前指令指针(IP)值。

在您的第一个代码示例中,偏移显然是0x00000000。它基本上是说

call +0 

printf实际地址还不知道,所以编译器只是把32位值0x00000000那里作为占位符。

这种零偏移的不完整调用自然会被解释为对当前IP值的调用。在你的平台上,IP是预先递增的,这意味着当一些指令被执行时,IP包含下一条指令的地址。即当执行地址0xE的指令时,IP包含值0x13。并且call +0自然被解释为对指令0x13的调用。这就是为什么你看到0x13在反汇编的不完整的代码。

代码完成后,占位符0x00000000偏移量将被替换为代码中printf函数的实际偏移量。偏移可以是正向(正向)或负向(向后)。在你的情况下,通话时的IP是0x4004DF,而printf功能的地址是0x4003C0。因此,机器指令将包含一个等于0x4003C0 - 0x4004DF的32位偏移值,该值为负值-287。所以,你在代码中看到什么是真正

call -287 

-2870xFFFFFEE1是二进制。这正是你在机器代码中看到的。只是您使用的工具向后显示。