【问题标题】:Assembly - x86 call instruction and memory address?汇编 - x86 调用指令和内存地址?
【发布时间】:2015-10-27 10:10:45
【问题描述】:

我一直在阅读一些汇编代码,并开始看到调用指令实际上是程序计数器相关的。

但是,每当我使用 Visual Studio 或 Windbg 进行调试时,它总是说 call 0xFFFFFF ... 这对我来说意味着我要跳转到那个地址。

谁是对的? Visual Studio 是否隐藏了指令编码的复杂性,只是说哦,这就是程序的意思,即调试器知道它是一条与 pc 相关的指令,并且由于它知道 pc,它只是为你做数学运算?

非常困惑。

【问题讨论】:

  • 是的,在反汇编跳转和调用指令时,反汇编程序会向您显示绝对目标地址,因为这就是您在汇编中编写指令的方式。
  • 但这就是我感到困惑的部分,看起来这不是你编写程序集的方式。编译器会发出 E8 cd -XXX 或其他东西。所以编译器作者实际上写了一个与 pc 相关的东西。我想一个后续问题可能是您的用户如何知道编译器是调用 0xFFFF 还是调用 pcRelative?
  • 我是说有一种叫做汇编的语言,人们用它来编写汇编程序。在这种语言中,跳转和调用指令使用绝对地址(例如call _foocall 0x12345),并且汇编器生成适当的机器语言编码。反汇编程序逆转了这个过程。代码实际上不是由汇编程序生成的事实并没有改变反汇编程序的工作方式。

标签: assembly linker x86 nasm masm


【解决方案1】:

如果您要反汇编尚未链接的.o 目标文件,则调用地址将只是由链接器填写的占位符。

您可以使用objdump -drwc -Mintel显示来自.o 的重定位类型+符号名称(-r 选项是关键。或者-R对于已链接的共享库。)


显示跳转目标的实际地址对用户来说更有用,而不是反汇编成jcc eip-1234H什么的。目标文件有一个默认的加载地址,因此反汇编程序在每条指令中都有一个eip 的值,这通常出现在反汇编输出中。

例如在我编写的一些 asm 代码中(我使用将其放入目标文件的符号名称,因此循环分支目标实际上对反汇编程序可见):

objdump -M intel  -d rs-asmbench:
...
00000000004020a0 <.loop>:
  4020a0:       0f b6 c2                movzx  eax,dl
  4020a3:       0f b6 de                movzx  ebx,dh
   ...
  402166:       49 83 c3 10             add    r11,0x10
  40216a:       0f 85 30 ff ff ff       jne    4020a0 <.loop>

0000000000402170 <.last8>:
  402170:       0f b6 c2                movzx  eax,dl

请注意,jne 指令的编码是 -0xD0 字节的有符号 little-endian 32 位位移。 (跳转后将其位移加到e/rip的值上。跳转指令本身有6个字节长,所以位移必须是-0xD0,而不仅仅是-0xCA。)0x100 - 0xD0 = 0x30,就是这个值2 的补码位移的最低有效字节。

在您的问题中,您所说的调用地址是0xFFFF...,这没什么意义,除非这只是一个占位符,或者您认为置换中的非0xFF 字节是操作码的一部分。

在链接之前,对外部符号的引用如下所示:

objdump -M intel -d main.o
  ...
  a5:   31 f6                   xor    esi,esi
  a7:   e8 00 00 00 00          call   ac <main+0xac>
  ac:   4c 63 e0                movsxd r12,eax
  af:   ba 00 00 00 00          mov    edx,0x0
  b4:   48 89 de                mov    rsi,rbx
  b7:   44 89 f7                mov    edi,r14d
  ba:   e8 00 00 00 00          call   bf <main+0xbf>
  bf:   83 f8 ff                cmp    eax,0xffffffff
  c2:   75 cc                   jne    90 <main+0x90>
  ...

注意call 指令的相对位移 = 0。因此,在链接器插入实际相对值之前,它们会在调用后立即对带有指令目标的call 进行编码。 (即RIP = RIP+0)。 call bf 之后紧跟一条指令,该指令从该部分的开头以0xbf 开始。另一个call 具有不同的目标地址,因为它位于文件中的不同位置。 (gcc 将main 放在它自己的部分:.text.startup)。

因此,如果您想了解实际调用的内容,请查看链接的可执行文件,或获取已查看目标文件符号的反汇编程序,以插入调用目标的符号名称,而不是将它们显示为调用零位移。

到本地符号的相对跳转在链接之前已经得到解决:

objdump -Mintel  -d asm-pinsrw.o:
0000000000000040 <.loop>:
  40:   0f b6 c2                movzx  eax,dl
  43:   0f b6 de                movzx  ebx,dh
  ...
 106:   49 83 c3 10             add    r11,0x10
 10a:   0f 85 30 ff ff ff       jne    40 <.loop>
0000000000000110 <.last8>:
 110:   0f b6 c2                movzx  eax,dl

注意,相对跳转到同一文件中的符号的指令编码完全相同,即使该文件没有基地址,所以反汇编程序只是将其视为零。

有关指令编码,请参阅英特尔的参考手册。链接https://stackoverflow.com/tags/x86/info。即使在 64 位模式下,call 也仅支持 32 位符号扩展的相对偏移量。支持 64 位地址作为绝对地址。 (在 32 位模式下,支持 16 位相对地址,带有操作数大小的前缀,我想节省一个指令字节。)

【讨论】:

  • 您关于目标文件具有默认加载地址的声明可能已经解除了我在这里学习的障碍。我现在得出的结论是,因为目标文件有一个单一的起始地址,并且所有函数都与该地址相关(因为它们只是)当加载目标文件时,该对象中的所有函数都被重新定位了某个数字?
  • @halivingston:是的。 4020a0 地址不是凭空出现的。这就是运行该文件时该指令将在进程的虚拟内存中的位置。这就是反汇编程序选择该地址的原因。 (ASLR 改变了一些事情,这意味着您必须对实际使用绝对地址而不是 RIP 相对的代码禁用地址空间布局随机化。)但是,在链接之前,.o / .obj 目标文件没有地址,只是将这些重定位写入机器代码的符号和位置列表。
  • 谢谢,彼得。您的最后陈述提示的最后一个问题:“实际使用绝对地址的代码” - 我开始意识到,即使是编写程序集的人,只是说调用 FooFunction,他们没有给出地址,事实上,他们没有甚至没有选择说使用操作码 E8(相对)或操作码 FF(绝对),他们只是说调用 FooFunction。我的问题是谁决定放置操作码 E8?还是FF?是不是说平台组装商说哦,我在英特尔上让我把它变成 E8?我的印象是,如果我正在编写我的编译器,我必须从字面上决定哪个操作码?
  • @halivingston:我更新了一些答案。回复:操作码。装配工决定。 call foo 的默认语法总是会生成一个相对调用。你会从call *foo_funcptrcall *eax 得到FF。 (我可能在这里混淆了 Intel / AT&T 语法。)请注意,FF 是一个 indirect 调用。通过函数指针调用。 RIP = [foo_funcptr],不是RIP = foo_funcptr。在 64 位模式下,[foo_funcptr] 将为指针生成一个 RIP 相对地址。像call *[rdi + rax*8] 这样的跳转表也是可以的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-01-15
  • 1970-01-01
  • 1970-01-01
  • 2011-10-27
  • 2015-06-25
  • 2015-11-01
相关资源
最近更新 更多