直接读取程序计数器

36

在内核模式或其他模式下,能否直接(即无需“技巧”)读取Intel CPU上的程序计数器?


1
相关:为什么不能直接设置指令指针?。你可以;这个指令叫做jmp。我的回答解释了为什么x86机器码被设计成不能像在ARM上一样编码一个moveip,因为PC是通用整数寄存器之一。 - Peter Cordes
7个回答

44

不,EIP / IP 不能直接访问,但在位置相关的代码中,它是一个链接时间常量,因此您可以使用附近(或远离)的符号作为立即数。

   mov eax, nearby_label    ; in position-dependent code
nearby_label:

要在位置无关的32位代码中获取EIP或IP:

        call _here
_here:  pop eax
; eax now holds the PC.

在比 Pentium Pro 新的 CPU 上(或者可能是 PIII),在rel32=0的情况下调用rel32是特殊处理的,不会影响返回地址预测器堆栈。因此,在现代 x86 上既高效又紧凑,这也是 clang 用于32位位置无关代码的方法。
在旧的32位 Pentium Pro CPU 上,这将不平衡呼叫/返回预测器堆栈,因此最好调用实际返回的函数,以避免在父函数中对未来15个或左右的ret指令进行分支错误预测。(除非您不打算返回,或者很少返回才不重要)。但是,返回地址预测器堆栈将恢复正常。
get_retaddr_ppro:
    mov  eax, [esp]
    ret                ; keeps the return-address predictor stack balanced
                       ; even on CPUs where  call +0 isn't a no-op.

在x86-64模式下,可以使用RIP相关的lea指令直接读取RIP。
default rel           ; NASM directive: use RIP-relative by default

lea  rax, [_here]     ; RIP + 0
_here:

MASM或GNU .intel_syntax语法: lea rax, [rip]

AT&T语法: lea 0(%rip), %rax


9
这段代码实际上会影响返回值分支预测,并且会显著降低程序的运行速度。我会尝试找一些相关的参考资料…… - Adam Rosenfield
2
不知道为什么这个回答比TrayMan的回答被接受。TrayMan的版本没有意外副作用,而且更短。 - Skizz
1
http://www.ptlsim.org/Documentation/html/node31.html 上有一个关于“返回地址栈”的很好的描述。 - mfazekas
5
关于这段代码,实际上会干扰返回值分支预测...我会尝试找一个参考资料... 参考资料是“英特尔64-ia-32优化手册” -> 3.4.1.4 内联、调用和返回 ->“返回地址堆栈机制增强了静态和动态预测器的功能,以便专门优化调用和返回。它包含16个条目,足以覆盖大多数程序的调用深度...为了启用返回堆栈机制,调用和返回必须成对匹配。” - Xtra Coder
1
@AdamRosenfield 这里有链接 https://blogs.msdn.microsoft.com/oldnewthing/20041216-00/?p=36973/ - phuclv
显示剩余10条评论

28

如果您需要特定指令的地址,通常可以使用类似以下代码:

thisone: 
   mov (e)ax,thisone

(注:在某些汇编器上,这可能会做错事情并从 [thisone] 读取一个字,但通常有一些语法可以让汇编器做正确的事情。)

如果您的代码是静态加载到特定地址的,则汇编器已经知道(如果您告诉它正确的起始地址)所有指令的绝对地址。动态加载的代码,例如作为任何现代操作系统上应用程序的一部分,将通过动态链接器执行地址重定位而获得正确的地址(前提是汇编器足够聪明以生成重定位表,它们通常会这样做)。


我尝试在x86-64上使用inline gcc汇编语言,但是在使用llvm 6.1.0时出现了“后端错误:64位模式不支持32位绝对寻址”。这是LLVM的问题还是64位模式下不可行? - csl
2
@csl:你要么在使用OS X(在这种情况下,除了使用lea rax,[thisone]之外,没有其他解决方法),要么在Linux上制作共享对象,因此这也无法工作。 (mov-immediate需要链接时常量地址,因此它仅适用于位置相关代码)。但是,如果您正在制作Linux可执行文件,则您的编译器可能默认制作位置无关的可执行文件,并且-no-pie -fno-pie可以制作位置相关的可执行文件,您可以使用绝对寻址 - Peter Cordes

17

在x86-64上,你可以这样做:

lea rax,[rip] (48 8d 05 00 00 00 00)

谢谢!这些数字的意思是什么? - Liran Orevi
这是指令编码 - 隐含着一个32位的偏移量为0,我不确定是否有更短的编码。 - matja
3
在NASM 2.10中,lea rax, [rip]无法工作。看起来RIP只能与rel一起间接使用,例如lea rax, [rel _start] - Ciro Santilli OurBigBook.com

9

在x86上没有直接读取指令指针(EIP)的指令。您可以通过一些内联汇编获取正在汇编的当前指令的地址:

// GCC inline assembler; for MSVC, syntax is different
uint32_t eip;
__asm__ __volatile__("movl $., %0", : "=r"(eip));

"

. 汇编指令会被汇编器替换为当前指令的地址。需要注意的是,如果你将上面的代码段放在函数调用中,每次都会得到相同的地址(在该函数内部)。如果你想要一个更可用的C函数,可以使用一些非内联汇编:

"
// In a C header file:
uint32_t get_eip(void);

// In a separate assembly (.S) file:
.globl _get_eip
_get_eip:
    mov 0(%esp), %eax
    ret

这意味着每次想要获取指令指针时,效率都会稍微降低,因为需要额外的函数调用。请注意,以这种方式进行操作不会使返回地址堆栈(RAS)炸掉。返回地址堆栈是处理器内部使用的另一个返回地址堆栈,用于为RET指令方便地进行分支目标预测
每次有CALL指令时,当前的EIP会被推送到RAS上,每次有RET指令时,RAS都会被弹出,并且顶部值将用作该指令的分支目标预测。如果搞砸了RAS(例如未能将每个CALL与RET匹配,如Cody's solution中所示),则程序将出现大量不必要的分支错误预测,导致程序减速。由于具有匹配的CALL和RET指令对,因此此方法不会使RAS炸掉。

非常感谢提供的信息,我不知道有两个栈.. :) - Liran Orevi
1
RAS是处理器使用的内部堆栈;它无法以任何方式被代码访问。它仅用于分支目标预测。没有它,代码仍然可以正确运行,只是速度会更慢。 - Adam Rosenfield
非常感谢你。如果您在Push之后手动设置ESP,RAS会出现问题吗? - Liran Orevi
或者只需获取当前函数的地址 uintptr_t func_addr = (uintptr_t)&function_name;。在x86 C/C++实现中,函数指针只是代码地址。这将组装为所需的任何内容。 - Peter Cordes

3

2
这不完全是汇编 :-) - Gunther Piez
1
应该是 &&current_address_label,而不是 $$ - phuclv
这不是相对地址(而不是IP地址)吗? - Peter Mortensen
@PeterMortensen:我不知道有人可以用C函数内部的某些代码地址做什么有用的事情,但这似乎和asm("movl $., %0", : "=r"(eip))答案一样好(如果不是更好,因为这个答案也适用于PIC代码。在这种情况下,编译器将不得不使用RIP相对LEA或32位PIC方法来获取静态地址,但当执行通过该标签时,它将与EIP匹配,除非您以某种方式复制了代码,破坏了静态寻址。) - Peter Cordes

0

你也可以从 /proc/stat 中读取这个信息。请查看 proc 的 man 手册。


我认为你指的是/proc/self/stat,引用手册也很酷。 - Ciro Santilli OurBigBook.com
1
/proc/self/stat 根据 man proc(5),有一个字段叫做 kstkeip %lu:当前的 EIP(指令指针)。 - Paul Praet
你能扩展一下这个答案吗?例如,包括一些背景信息(这只适用于Linux吗?)并考虑其他评论中的信息。 - Peter Mortensen
1
如果你按照“正常”的方式做,你不是总会得到libc的read()函数中syscall指令的地址吗?这似乎对于找出自己的EIP/RIP值没有什么用处。 - Peter Cordes

-1

有一种简单的方法可以改变程序计数器(eip)。

当你用“call”调用一个函数时,eip被推入堆栈中,然后当你用“ret”返回时,eip就从堆栈中弹出。因此,你需要做的就是将你想要的值压入堆栈,然后使用“ret”。

例如:

mov eax, 0x100
push eax`
ret

这就完成了。


1
这是对另一个问题的答案(该问题已存在:为什么不能直接设置指令指针?)。我不知道为什么你要使用push/ret,而不是简单地使用jmp eax - Peter Cordes

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