4

好的,一切从这里开始:无符号整数和无符号字符保持相同的值但行为不同,为什么?

我编写了以下应用程序来了解幕后发生的事情(即编译器如何处理此问题)。

#include <stdio.h>

int main()
{
  {
  unsigned char k=-1;
  if(k==-1)
  {
    puts("uc ok\n");
  }
  }

  {
  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 ok\n" (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 不跳过它?优化被禁用,但似乎它仍然优化了一些东西......

4

4 回答 4

7

我的猜测(我不是 GCC 开发人员)是它做了足够的静态分析来证明 firstif的测试永远不会是真的。

这应该不会太难,因为在初始化和测试之间没有代码,任何副作用或外部实体都无法更改变量。

只是出于好奇,请尝试制作变量static和/或volatile查看是否有任何变化。

于 2013-05-27T12:05:14.533 回答
2

它看起来像是 GCC 的一个问题,尽管不可否认这是一个非常小的问题。

来自GCC 的文档网站(重点是我的):

在没有任何优化选项的情况下,编译器的目标是降低编译成本并使调试产生预期的结果。语句是独立的:如果您在语句之间使用断点停止程序,则可以为任何变量分配一个新值或将程序计数器更改为函数中的任何其他语句,并从源代码中获得您期望的结果。

因此,-O0您应该能够在unsigned char k=-1;and之间放置一个断点if(k==-1),在该断点 modify 期间k,并期望采用该分支;但这对于发出的代码是不可能的。

于 2013-05-27T12:21:55.440 回答
1

更新:我的猜测是char,作为低于基本(int)类型的类型,被简单地升级为整数类型进行比较。(假设编译器将文字作为整数,并且通常更喜欢字大小的整数而不是字节大小的整数)

并且作为一个无符号值,零扩展总是正的(注意MOVZX而不是有符号的变体!),因此检查可能通过基本的常量传播进行了优化。

您可以尝试将文字强制为字节(强制转换或后缀),例如与 ((unsigned char)(-1)) 进行比较,然后编译器可能会插入一个 1 字节的比较,结果可能会有所不同。

于 2013-05-27T12:05:18.627 回答
0

这里有很多要点:

  • 编译器甚至不必查看 k 的初始化来证明条件 k==-1 在 unsigned char 情况下永远不会为真。关键是,无符号的8 位值需要提升为 32 位,因为比较的右侧是一个整数常量,默认为 32 位。因为 k 是无符号的,所以这个提升的结果将是00000000 00000000 00000000 xxxxxxxx。常数 -1 具有位模式11111111 11111111 11111111 11111111,所以不管是什么xxxxxxxx,比较的结果总是错误的。
  • 在这一点上我可能是错的,但我相信即使 k 被指定为 volatile,编译器也只需要将它加载到寄存器中(因为加载操作可能会在硬件中触发一些期望的副作用),而不是实际执行比较或为无法访问的 if 块生成代码。
  • 实际上,对于无法访问的代码省略生成汇编完全符合-O0 加速编译过程的目标。
  • AFAIK,无符号和负常数之间的比较无论如何都是未定义的行为。至少,根本没有机器指令可以正确处理这种情况,并且编译器不会在软件中插入必要的代码来处理它,正如您从反汇编中看到的那样。你得到的只是有符号和无符号之间的隐式转换,导致整数溢出(这本身就是未定义的行为),以及未混合符号的比较。
于 2013-06-02T14:25:08.547 回答