UCX 中有这么一段注释:
/*
* Since we can't assume the new code will be within 32-bit
* range of the global variable argument, we need to translate
* the code from:
* cmpl $imm32, $disp32(%rip)
* to:
* push %rax
* movq $addr64, %rax ; $addr64 is $disp32+%rip
* cmpl $imm32, (%rax)
* pop %rax
*/
为了进入函数时,cmpl 指令本来的语义能够正确执行,将函数开头的这个 cmpl 更换成了 四条指令,对寻址地址做了扩容。下边来分析背后的原理。
如前所言,ucx 中的这段注释描述了在 x86-64 汇编语言中处理指令时的一种方法,特别是在需要访问全局变量或内存地址时,如何处理指令的地址范围问题。
接下来对这段注释背后的汇编语言原理做详细解释,
1,背景
在 x86-64 架构中,指令的寻址方式可以使用相对寻址(如 (%rip))来访问内存。%rip 是指令指针寄存器,指向当前执行的指令。使用 cmpl $imm32, $disp32(%rip) 指令时,disp32 是一个 32 位的偏移量,表示从当前指令位置向后偏移的地址。
2,问题
32 位范围限制:在某些情况下,disp32 可能超出了 32 位有符号整数的范围(即 -2^31 到 2^31-1),这意味着无法直接使用 32 位偏移量来访问全局变量或其他内存地址。
解决方案
为了处理这个问题,注释中提到的替代方案是使用 push、movq、cmpl 和 pop 指令。具体步骤如下:
1,保存寄存器状态:
push %rax
- 将 rax 寄存器的当前值压入栈中,以便后续恢复。rax 通常用于存储临时值。
2, 计算目标地址:
movq $addr64, %rax ; $addr64 is $disp32 + %rip
- 将目标地址($addr64)加载到 rax 中。这里的 $addr64 是通过将 disp32 加上当前指令的地址(%rip)计算得出的。这允许访问超出 32 位范围的全局变量或内存地址。
3. 执行比较操作:
cmpl $imm32, (%rax)
- 使用 cmpl 指令比较 imm32(立即数)与 rax 指向的内存地址的值。这里,%rax 现在指向了正确的内存地址。
4. 恢复寄存器状态:
pop %rax
- 从栈中弹出之前保存的 rax 值,恢复寄存器的状态。
3,原理总结
寄存器和内存访问:通过将目标地址加载到寄存器中,可以避免直接使用可能超出范围的 32 位偏移量。这种方法利用了寄存器的灵活性,使得可以访问更大的地址空间。
栈操作:使用 push 和 pop 指令来保存和恢复寄存器状态是汇编语言中的常见做法,确保在执行过程中不丢失重要的寄存器值。
相对寻址与绝对寻址:通过计算绝对地址($addr64),可以在不受 32 位限制的情况下访问内存。这种方法在处理大型程序或库时非常有用,尤其是在需要访问全局变量或动态分配内存时。
总体回顾
这段注释描述的汇编语言原理展示了如何在 x86-64 架构中处理指令的地址范围问题。通过使用 push、movq、cmpl 和 pop 指令,可以安全地访问超出 32 位范围的内存地址,同时保持寄存器状态的完整性。这种方法在编写高效和安全的汇编代码时非常重要。