Skip to content

Latest commit

 

History

History
94 lines (82 loc) · 3.65 KB

File metadata and controls

94 lines (82 loc) · 3.65 KB

C++ 未定义的行为

  • UB(Undefined behavior):程序的行为没有限制。常见的未定义行为例子包括:数组访问内存越界,有符号数溢出,间接访问空指针,在一个表达式中无顺序多次修改标量,用不同类型的指针访问对象
  • UB and optimization
    • 正确的 C++ 程序是没有未定义行为的,所以当编译器优化了含有 UB 的代码,程序会出现不可预料的结果

    • 有符号数溢出,下面是 C++ 代码和对应可能生成的机器语言

      • 没有溢出时返回 1,溢出时是 UB,编译器可能优化,每次都返回 1
      int foo(int x)
      {
          return x+1 > x; // either true or UB due to signed overflow
      }
      
      foo(int):
          movl    $1, %eax
          ret
      
    • 访问越界,下面是 C++ 代码和对应可能生成的机器语言

      • 访问下标在 0-3 时,如果存在元素 v 返回true,否则会访问越界,编译器可能优化,每次都返回 true,也不会访问越界
      int table[4] = {};
      bool exists_in_table(int v)
      {
          // return true in one of the first 4 iterations or UB due to out-of-bounds access
          for (int i = 0; i <= 4; i++)
          {
              if (table[i] == v) return true;
          }
          return false;
      }
      
      exists_in_table(int):
          movl    $1, %eax
          ret
      
    • 未初始化的标量,下面是 C++ 代码和对应可能生成的机器语言

      • 当 x 非 0 时,a 会被赋值 42,否则 a 未初始化,编译器可能优化,每次都将 a 赋值42,然后返回
      std::size_t f(int x)
      {
          std::size_t a;
          if(x) // either x nonzero or UB
              a = 42;
          return a;
      }
      
      f(int):
          mov     eax, 42
          ret
      
    • 间接访问空指针,下面是 C++ 代码和对应可能生成的机器语言

      • 函数foo:当 p 是空指针时,x 的赋值是间接访问空指针。否则返回 0。编译器可能优化,每次返回 0 而不会访问到空指针
        • xorl %eax,%eax按位异或,相当于清 0,将寄存器%eax设置为 0。也可以使用movl $0,%eax,但是前者需要 2 个字节,后者需要 5 个字节
      • 函数bar:直接访问空指针指向的值是 UB,编译器可能优化,每次直接执行下一行代码
        • retq等同于addq $8,%rsp; jmpq -8(%rsp)retq%esp指向的返回地址弹出,存入寄存器%eip
        • 寄存器%eip是程序计数器,存储了 CPU 要读取指令的地址,即 CPU 将要执行的指令的地址。每次 CPU执行完相应的汇编指令后,%eip的值就会增加
        • 寄存器%esp是栈指针指向栈顶元素。栈向低地址方向增长,可以通过增加栈指针来释放空间
        • 函数调用时会先将返回地址入栈,即程序中紧跟在调用函数后面的那条指令的地址,所以栈顶指针%esp指向的就是调用函数后面的那条指令的地址,retq会将该地址存入%eip,CPU 就会继续往后执行
        • 在 64-bit 时,ret会从栈中弹出四字节的地址保存到寄存器%eip
        • 在 32-bit 时,ret会从栈中弹出两字节的地址保存到寄存器%eip
      int foo(int* p)
      {
          int x = *p;
          if(!p) return x; // Either UB above or this branch is never taken
          else return 0;
      }
      int bar()
      {
          int* p = nullptr;
          return *p;        // Unconditional UB
      }
      
      foo(int*):
          xorl    %eax, %eax
          ret
      bar():
          retq