Linux内核0.12中的代码片段使用如下函数参数:
int do_signal(int signr, int eax /* other parameters... */) {
/* ... */
*(&eax) = -EINTR;
/* ... */
}
代码的目的是将-EINTR放到eax所在的内存中,但我不知道为什么只要分配给eax它就不会起作用:
eax = -EINTR
编译器如何区分 eax 和 *(& eax)?
答案 0 :(得分:31)
你发布的旧Linux试图执行一个非常脆弱的黑客攻击。该函数的定义如下:
int do_signal(long signr,long eax,long ebx, long ecx, long edx, long orig_eax,
long fs, long es, long ds,
long eip, long cs, long eflags,
unsigned long * esp, long ss)
函数参数实际上并不代表函数的参数(signr
除外),但调用函数(在程序集中编写的内核中断/异常处理程序)的值在调用{{1}之前保留在堆栈上}}。 do_signal
语句用于修改堆栈上EAX的保留值。类似地,语句*(&eax) = -EINTR
用于修改调用处理程序的返回地址。在*(&eip) = old_eip -= 2
返回后,处理程序从堆栈中弹出前9个“参数”,将它们恢复到指定的寄存器。然后它执行一个do_signal
指令,从堆栈中弹出剩余的参数并返回用户模式。
毋庸置疑,这种黑客行为令人难以置信地不可靠。它依赖于编译器生成完全符合预期的代码。我很惊讶它甚至在那个时代的GCC编译器中工作,我怀疑它早在GCC引入了一个打破它的优化之前。
答案 1 :(得分:22)
一种可能的意图是将eax
变量保留在寄存器之外。如果我们查看C99 draft standard,我们会看到6.5.3.2
地址和间接运算符 部分(强调我的):
一元&运算符产生其操作数的地址。 [...]如果操作数是一元*运算符的结果,则不是 操作员和&运算符被评估,结果就像两者一样 被忽略,除了对运算符的约束仍然适用 结果不是左值。[...]
脚注 87 它说(强调我的前进):
因此,& * E等效于E(即使E是空指针),和 &(E1 [E2])至((E1)+(E2))。 如果E是一个函数,那总是如此 指示符或左值,它是一元&的有效操作数。 运算符,*& E是函数指示符或等于E 的左值。
我们在& operator
上找到以下约束:
一元&的操作数运算符应该是一个函数 指示符,[]或一元*运算符的结果,或者左值 指定一个不是位字段且未声明的对象 寄存器存储类说明符。
这是有道理的,因为我们无法获取寄存器的地址,因此通过执行地址操作,他们可能一直试图阻止编译器完全在寄存器中执行操作并确保特定存储位置中的数据被修改。
正如ouah指出的那样,这并不妨碍编译器优化实际的 no-op ,但正如GCC hacks in the Linux kernel中所述。 Linux依赖于许多gcc
扩展,并认为0.12
是一个非常古老的内核gcc
可能已经保证了这种行为或者可能已经可靠地以这种方式工作但我找不到任何文档这样说。