【问题标题】:Why use a function parameter 'foo' in this way: *(&foo)?为什么以这种方式使用函数参数'foo':*(&foo)?
【发布时间】:2014-08-27 14:39:28
【问题描述】:

Linux 内核 0.12 中的代码 sn-p 使用如下函数参数:

int do_signal(int signr, int eax /* other parameters... */) {
    /* ... */

    *(&eax) = -EINTR;

    /* ... */
}

代码的目的是将 -EINTR 放入 eax 所在的内存中,但我不知道为什么如果只是分配给 eax 它将不起作用:

eax = -EINTR

编译器如何区分 eax*(&eax)

【问题讨论】:

  • 你能给我们提供那个内核源的链接吗?或添加函数的完整定义(如果它不太长)。
  • 可能是为了防止编译器对eax使用寄存器,强制它使用栈上的内存。
  • @KlasLindbäck:很好!!!你应该把它写下来,因为它可能是正确的答案(eax 这个名字实际上暗示这就是原因)。
  • @barakmanos Shafik Yaghmour 已经发布了一个很好的答案,其中引用了标准中的相关部分,所以我只会赞成他的答案。

标签: c compiler-construction


【解决方案1】:

您发布的旧 Linux 试图执行非常脆弱的 hack。函数定义如下:

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 除外),而是调用函数(用汇编编写的内核中断/异常处理程序)在调用 do_signal 之前保存在堆栈中的值。 *(&eax) = -EINTR 语句用于修改堆栈上 EAX 的保留值。类似地,*(&eip) = old_eip -= 2 语句用于修改调用处理程序的返回地址。在do_signal 返回后,处理程序将前 9 个“参数”从堆栈中弹出,将它们恢复到指定的寄存器。然后它执行一条IRETD 指令,将剩余的参数从堆栈中弹出并返回到用户模式。

不用说,这种 hack 非常不可靠。它依赖于编译器完全按照他们期望的方式生成代码。我很惊讶它甚至可以在那个时代的 GCC 编译器中工作,我怀疑 GCC 很久之前就引入了破坏它的优化。

【讨论】:

  • 我很惊讶即使是旧的 gcc 版本在 eax = -EINTR*(&eax) = -EINTR 并启用优化后的行为也会有所不同。
  • 是的,在现代系统上编译旧内核确实很麻烦,但是有人已经完成了,请参阅Oldlinux.org
  • 如果您获取它们的地址,旧版本的 GCC 不会将内容放入注册器中。 (请注意,此行为不是标准所要求的,只是如果您获取它的地址,则不能明确声明 register。)编译器可能仍然消除或移动了赋值,因为它是一个简单的常量并且并非所有代码路径都使用它。
【解决方案2】:

一个可能的意图可能是将eax 变量保留在寄存器之外。如果我们查看C99 draft standard,我们会看到6.5.3.2 部分地址和间接运算符 说(强调我的):

一元 & 运算符产生其操作数的地址。 [...]如果操作数是一元 * 运算符的结果,那么 运算符和 & 运算符都被求值,结果是 就好像两者都一样 被省略,除了对运算符的限制仍然适用 结果不是左值。[...]

在脚注 87 中说(强调我的未来):

因此,&*E 等价于 E(即使 E 是空指针),并且 &(E1[E2]) 到 ((E1)+(E2))。 如果 E 是一个函数,它总是正确的 指示符或作为一元 & 的有效操作数的左值 运算符,*&E 是函数指示符或等于 E 的左值

我们在& operator 上找到以下约束:

一元 & 运算符的操作数应为函数 指示符,[] 或一元 * 运算符的结果,或左值 指定一个不是位域 且未声明为的对象 寄存器存储类说明符。

这是有道理的,因为我们无法获取寄存器的地址,因此通过执行 address of 操作,他们可能一直试图阻止编译器完全在寄存器中执行操作并确保特定内存位置的数据被修改。

正如 ouah 指出的那样,这不会阻止编译器优化实际上是 no-op 的东西,但正如 GCC hacks in the Linux kernel 中所记录的那样。 Linux 依赖于许多 gcc 扩展,并且考虑到 0.12 是一个非常旧的内核 gcc 可能已经保证了这种行为,或者可能意外地以这种方式可靠地工作,但我找不到任何这样说的文档。

【讨论】:

  • +1,这是一个很好的答案,但是是什么阻止了编译器仍然使用eax 的寄存器并将*& 视为无操作?这很可能是针对特定编译器的 hack。
  • 不错的答案,确实。但是& 的操作数在这里不是一元* 的结果,而是反过来(我们有*&eax,而不是&*eax),我在标准中找不到* 的类似保证.也许这部分回答了 Ouah 的担忧?
  • 我还想补充一点,register 的缺失对编译器没有任何意义。约束适用于声明,但不适用于实际放置值的位置。据我所知,没有办法保证值在标准 C99 中的位置,即使 register 关键字本身也只是对编译器的建议。也许如果实际使用了地址,那么它必须去内存,但我不确定优化不能改变这一点。
  • @mafso 很好,我搞砸了订单,我把它搞砸了,因为我也无法证明它也能以另一种方式工作。
  • @ShafikYaghmour *& 在 c11, 6.5.3.2p4 中作为脚注出现:“如果 E 是函数指示符或作为一元 & 运算符的有效操作数的左值,则总是正确的, *&E 是函数指示符或等于 E 的左值。"
猜你喜欢
  • 1970-01-01
  • 2013-01-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-18
  • 2011-11-23
  • 2015-09-26
相关资源
最近更新 更多