考虑这个添加常数的简单函数:
unsigned char f(unsigned char x) {
return x + 5;
}
这将生成以下程序集(在gcc 4.7.2上使用-O3
):
leal 5(%rdi), %eax
ret
既然无符号溢出在C中是明确定义的行为,人们会认为添加模运算本质上应该是一个nop:
unsigned char f(unsigned char x) {
return (x + 5) % 256; // assume char is 8-bits, which is typical
}
但是生成的程序集有一个额外的指令:
leal 5(%rdi), %eax
movzbl %al, %eax
ret
有人可以告诉我为什么会这样吗?虽然我对装配不是很熟悉。
(注意:这只是我用来理解GCC如何优化代码的玩具问题。)
答案 0 :(得分:4)
对于“为什么生成的代码不同”的确切答案,您可能需要一位熟悉此gcc
编译器详细信息的工程师。您可能希望通过以下几个示例进行更多实验:
unsigned char f1(unsigned char x) { return x + 5; }
unsigned char f2(unsigned char x) { return (x + 5) % 256; }
unsigned char f3(unsigned char x) { return (x + 5) % 256U; }
unsigned char f4(unsigned char x) { return (x + 5) & 0xFFU; }
gcc
版本4.1.2适用于64位系统,我得到所有这些函数的相同代码,64位为32位代码。其中实际上包括movzbl
。这可能是gcc
编译f1
中的错误(可能在来电方面得到纠正)。它实际上取决于调用约定:64位寄存器中的8位值是否应为零/符号扩展。我在2005年6月14日的 System V应用程序二进制接口,AMD64架构处理器补充的0.96版草案中找不到这个结论。gcc
编译器4.1.2似乎采用了“更安全而不是抱歉”的理念,因为movzbl
也出现在来电方面。根据我的经验,通常需要将这些值设置为零/符号扩展,除非有人对寄存器的某些部分进行操作,这是非常不寻常的。
有趣的是,我的家庭编译器gcc
版本4.3.2在确实通过和操作实现f2
方面确实有所不同。所有其他人只需添加5,强烈建议呼叫者有责任执行零/符号扩展,这确实如此。但这是32位代码。
如果我在任何架构规范中找到超大寄存器中值/零/符号扩展的确定答案,那么我会告诉您。我碰巧也需要专业地了解这一点。
为你的gcc
编译器辩护。您正在寻找小啤酒优化。普通代码不包含这样的模数,如果编译器下面的编译器将这种特殊模数减少到和,那就太好了。如果是%256
(vs %256U
),则需要进行一些值范围分析,以确定和是否足够,因为模数是在'signed'算术中完成的。很明显,我的编译器确实在某些时候得出结论和就足够了,但显然已经太晚了,无法确定它是否被结果输入所包含,而在其他情况下它确定了。这就是我们编译工程师所说的“相序问题”。
更新寄存器中值的零/符号扩展名。
我现在已经放弃了这个任务,并且必须继续与一些同事继续,因为如果参数/函数结果预计为零/符号扩展,我还没有找到确凿的陈述。
我确实在上述ABI规范中找到了与此相关的内容。
布尔值存储在内存对象中时,存储为单字节对象,其值始终为0(假)或1(真)。当存储在整数寄存器中或作为参数传递到堆栈时,寄存器的所有8个字节都是重要的;任何非零值都被视为真。
所以布尔类型必须为零扩展。
对于可能调用使用varargs或stdargs的函数的调用(无原型调用或对声明中包含省略号(...)的函数的调用)
%al
(注释14)用作隐藏参数来指定使用的SSE寄存器的数量。%al
的内容不需要与寄存器的数量完全匹配,但必须是所使用的SSE寄存器数量的上限,并且在0-8的范围内。注意14:请注意,
%rax
的其余部分未定义,仅定义了%al
的内容。
因此,%al
的这种特殊用法无需扩展。
考虑到布尔值必须为零扩展,可以得出结论,ABI的精神是其他子词类型也应该被扩展。采取更正式的立场可以说,没有任何陈述应该被解释为不需要零/符号扩展。总而言之,并不令人满意。
在寄存器中值的零/符号扩展时更新2.
我和一位同事讨论了这个问题。 2012版0.99的ABI的最新版本已经完全根据布尔值的参数传递进行了更改,因为这些只是零扩展到8位。这表明这已被修改为与传递其他子词类型一致,因为所有不零/符号扩展。 AMD64架构还支持64位寄存器中一半的子字寄存器,并可对这些子字寄存器进行操作。这可能是不以零/符号扩展方式传递参数的动机。