好,一切都从这里开始:无符号整数和无符号字符保持相同的值,但行为不同,为什么?
我写了下面的应用程序来理解幕后发生了什么(即编译器是如何处理这个问题的)。
#include <stdio.h>
int main()
{
{
unsigned char k=-1;
if(k==-1)
{
puts("uc okn");
}
}
{
unsigned int k=-1;
if(k==-1)
{
puts("ui ok");
}
}
}
在使用GCC编译时,如:
gcc -O0 -S -masm=intel h.c
我得到以下程序集文件:
.file "h.c"
.intel_syntax noprefix
.section .rodata
.LC0:
.string "ui ok"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 16
mov BYTE PTR [rbp-1], -1
mov DWORD PTR [rbp-8], -1
cmp DWORD PTR [rbp-8], -1
jne .L3
mov edi, OFFSET FLAT:.LC0
call puts
.L3:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
.section .note.GNU-stack,"",@progbits
令我惊讶的是,第一张支票甚至都不在那里。
但是,如果我用Microsoft Visual c++(2010)编译同样的东西,我得到(我已经从这个清单中删除了很多垃圾,这就是为什么它不是那么有效):
00B81780 push ebp
00B81781 mov ebp,esp
00B81783 sub esp,0D8h
00B81789 push ebx
00B8178A push esi
00B8178B push edi
00B8178C lea edi,[ebp-0D8h]
00B81792 mov ecx,36h
00B81797 mov eax,0CCCCCCCCh
00B8179C rep stos dword ptr es:[edi]
00B8179E mov byte ptr [k],0FFh
00B817A2 movzx eax,byte ptr [k]
00B817A6 cmp eax,0FFFFFFFFh
00B817A9 jne wmain+42h (0B817C2h)
00B817AB mov esi,esp
00B817AD push offset string "uc okn" (0B857A8h)
00B817B2 call dword ptr [__imp__puts (0B882ACh)]
00B817B8 add esp,4
00B817BB cmp esi,esp
00B817BD call @ILT+435(__RTC_CheckEsp) (0B811B8h)
00B817C2 mov dword ptr [k],0FFFFFFFFh
00B817C9 cmp dword ptr [k],0FFFFFFFFh
00B817CD jne wmain+66h (0B817E6h)
00B817CF mov esi,esp
00B817D1 push offset string "ui ok" (0B857A0h)
00B817D6 call dword ptr [__imp__puts (0B882ACh)]
00B817DC add esp,4
00B817DF cmp esi,esp
00B817E1 call @ILT+435(__RTC_CheckEsp) (0B811B8h)
问题是:为什么会这样?为什么GCC"跳过"第一个IF,我怎样才能强迫GCC不跳过它?优化是禁用的,但似乎它仍然优化掉了一些东西…
我的猜测(我不是GCC开发人员)是它做了足够的静态分析来证明自己第一个if
的测试从来都不是真的。
不应该太困难,因为在初始化和测试之间没有代码,没有任何副作用或外部实体可以改变变量。
只是出于好奇,尝试使变量static
和/或volatile
,看看是否有任何变化
这看起来像是GCC的一个问题,尽管承认是一个非常小的问题。
来自GCC的文档网站(强调我的):
如果没有任何优化选项,编译器的目标是减少编译成本,并使调试产生预期的结果。语句是独立的:如果您在语句之间使用断点停止程序,那么您可以为任何变量赋一个新值或将程序计数器更改为函数中的任何其他语句,并从源代码中获得您期望的结果。
因此,对于-O0
,您应该能够在unsigned char k=-1;
和if(k==-1)
之间放置一个断点,在该断点期间修改k
,并期望采取分支;但是对于发出的代码,这是不可能的。
Updated:我的猜测是,char作为基数(int)类型以下的类型,只是为了比较而升级为整数类型。(假设编译器将字面量作为整数,并且通常更喜欢单词大小的整数而不是字节大小的整数)
作为一个无符号值,零扩展总是正的(注意MOVZX而不是有符号的变体!),所以检查可能通过基本常数传播优化了。
你可以尝试强制文本为字节(强制转换或后缀),例如与((unsigned char)(-1))比较,然后编译器可能会插入一个1字节的比较,结果可能会不同。
这里有一些细微之处:
- 编译器甚至不需要查看k的初始化来证明条件k==-1在unsigned char的情况下永远不可能为真。关键是,unsigned 8位值需要提升为32位,因为比较的右侧是一个整数常量,默认为32位。因为k是无符号的,所以这次提升的结果是
00000000 00000000 00000000 xxxxxxxx
。常数-1的位模式是11111111 11111111 11111111 11111111
,所以不管xxxxxxxx
是什么,比较的结果总是假的。 在这一点上我可能是错的,但我相信即使k被指定为volatile,编译器也只需要将其加载到寄存器中(因为加载操作可能会触发硬件中的一些期望的副作用),而不是实际执行比较或为不可达的if块生成代码。 - 实际上,对于不可达的代码省略生成汇编完全符合- 0的目标,以加快编译过程。 当然,unsigned常量和负常量之间的比较是未定义的行为。至少,根本没有机器指令来正确处理这种情况,编译器不会插入必要的代码来处理它,正如您从反汇编中看到的那样。你得到的只是有符号和无符号之间的隐式强制转换,导致整数溢出(这本身就是未定义的行为),以及未混合符号的比较。