可执行文件和可重定位目标文件的疑惑

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。这意味着在地址13处调用函数,对吗?13有下一条指令,对吗?请解释一下这是什么意思?

与main函数相对应的可执行代码如下:

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 <printf@plt>
  4004df:       b8 00 00 00 00          mov    $0x0,%eax
  4004e4:       c9                      leaveq 
  4004e5:       c3                      retq 

这里被称为callq 4003c0,但二进制指令是e8 e1 fe ff ff。没有与4003c0对应的内容。我哪里弄错了吗?
谢谢。 Bala
3个回答

7
在第一个案例中,看一下指令编码 - 在函数地址所在的位置上全是零。这是因为对象尚未链接,因此外部符号的地址尚未连接。当您将最终链接到可执行格式时,系统会添加另一个占位符,然后动态链接器最终会在运行时添加正确的 printf() 地址。这是我写的一个“Hello, world”程序的快速示例。
首先,是目标文件的反汇编:
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 的调用转换而来的。这个重定位会在链接时被注意到并修复。在动态库链接的情况下,重定位和修复可能直到程序运行时才能得到完全解决,但我希望你可以从这个例子中理解。


7
< p > 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。因此,机器指令将包含一个32位偏移值,等于0x4003C0 - 0x4004DF,即负值-287。所以您在代码中看到的实际上是:

call -287

-287 在二进制中表示为 0xFFFFFEE1。这正是您在机器代码中看到的内容。只是您使用的工具将其倒置显示而已。


5

x86中的调用是相对的,如果有e8,那么调用位置是addr+5。

e1 fe ff ff是小端编码的相对跳转。它实际上表示fffffee1

现在将其加到调用指令的地址+5中:(0xfffffee1 + 0x4004da + 5) % 2**32 = 0x4003c0


1
+5是因为它相对于调用后的下一条指令,而调用长度为5个字节。 - caf
x86的调用可以是相对的或绝对的。只是E8是一个相对调用。 - AnT stands with Russia
是的,我忘记了还有绝对目标,但它们要么由段:选择器指定,要么由指向要跳转到的地址的指针指定。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接