除了其他问题之外,我认为还没有人提到一个普遍的问题:在内存中最终形成的代码通常无法重新定位。也许你的foo
函数可以,但考虑以下情况:
int main(int argc, char **argv) {
if (argc == 3) {
return 1;
} else {
return 0;
}
}
结果的一部分:
if (argc == 3) {
401149: 83 3b 03 cmpl $0x3,(%ebx)
40114c: 75 09 jne 401157 <_main+0x27>
return 1
40114e: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%ebp)
401155: eb 07 jmp 40115e <_main+0x2e>
} else {
return 0
401157: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
40115e: 8b 45 f4 mov -0xc(%ebp),%eax
}
注意
jne 401157 <_main+0x27>
。在这种情况下,我们有一个 x86 条件跳转指令
0x75 0x09
,它向前跳 9 个字节。因此它是可重定位的:如果我们将代码复制到其他地方,那么我们仍然希望向前跳 9 个字节。但是如果它是相对跳转或调用,则跳转到您复制的函数之外的代码?您将跳转到堆栈上某个任意位置。
并非所有跳转和调用指令都是这样的(不是所有体系结构,甚至不是 x86 的所有指令)。一些通过将地址加载到寄存器中,然后进行远跳转/调用来引用绝对地址。当代码准备执行时,所谓的“加载器”将通过填充目标实际在内存中具有的任何地址来“修复”代码。复制这样的代码将(最好)导致跳转或调用与原始代码相同的地址。如果目标不在您要复制的代码中,则可能是您想要的。如果目标在您要复制的代码中,则会跳转到原始代码而不是复制品。
相对地址和绝对地址的相同问题也适用于代码以外的其他事物。例如,对数据部分(包含字符串文字,全局变量等)的引用如果被相对寻址并且不是复制的代码的一部分,则会出错。
另外,函数指针不一定包含函数中第一条指令的地址。例如,在 ARM 处理器上的 ARM/thumb 交互工作模式下,thumb 函数的地址比其第一条指令的地址大 1。实际上,值的最低有效位不是地址的一部分,它是一个标志,告诉 CPU 在跳转时切换到 thumb 模式。