(WWDC) 理解 Undefined Behavior

本文探讨了Undefined Behavior的概念,举例说明了如除数为0、数组越界等问题,并阐述了编译器如何处理这类行为。文章指出,虽然Undefined Behavior可能导致不可预料的结果,但也有助于提高代码性能。此外,通过Static Analyzer和Runtime Sanitizers等工具,开发者可以检测并避免此类问题。在Swift中,尽管有更高的安全性,但仍需注意与C家族API交互时可能出现的Undefined Behavior。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


内容概览

  • 什么是 undefined behavior?
  • 编译器 和 undefined behavior
  • 安全性问题
  • 使用工具进行问题检测
  • Swift 更安全



什么是 undefined behavior?

本国际标准中未作任何规定的行为 —— ISO C++14 Standard

当你的代码语法正确,但是执行的行为不在语言准许的范围内,这种行为就是Undefined behavior。
比如:除数为0、数组越界、数值类型溢出、更改字符串字面量



编译器如何处理Undefined Behavior?

  • 诊断并给出 warning 或 error
  • 按照文档中的规则执行
  • 产生不可预料的结果



为什么会有Undefined Behavior?

1306450-9f165fc7c7387702.png

这其实是一个权衡的结果,C、C++、Objective-C这些语言更看重性能而不是安全。
也正因如此,我们的操作系统才能这么高效地运行。
不过,开发者需要为此付出代价。
所以,开发者需要知道这些行为并且知道如何解决他们。


Undefined Behavior 示例



使用未初始化的变量

int uninitialized_variable(int arg) {
    int value;
    if (arg <= 0)
        value = 42;
    return arg + value;
}

如果为该函数传入正数,就会导致 Undefined Behavior,因为value未初始化就被使用。
这时候编译器会捕获问题并发出warning。静态分析器(static analyzer) 可以针对更复杂的情况给出详细的信息。

1306450-17c26760dc213aea.png
编译器会捕获问题并发出warning



未对齐的指针

char *serialize_misaligned(char *buffer, int a, int b) {
    *(int *)buffer = a;
    buffer += sizeof(a);
    *(int *)buffer = b;
    buffer += sizeof(b);
    return buffer;
} 

该函数传入的是char类型的指针,但是使用时却变成了int类型。
问题在于,不是所有的char类型的指针都可以变成int类型的指针。
int类型需要对齐,也就是,在内存中的地址偏移量是4。

1306450-66fbc3e1d2121ac6.png

1306450-77b6e6ad49b22768.png
int类型需要对齐

在不同的硬件架构下迁移代码时,这个问题会更容易发生。因为不同的硬件有不同的对齐约束。
不过,从Xcode 9开始,你可以使用 Undefined Behavior Sanitizer 来捕获这些问题。

1306450-3e0106c574726eed.png
Undefined Behavior Sanitizer



编译器 和 Undefined Behavior



Undefined Behavior 可以提供信息

1306450-5476506b5cb78795.png

在第一行中,因为Undefined Behavior指出Signed int不能溢出,所以编译器就可以假设 x 一定小于 x + 1
所以编译器可以直接按照整数偏移量去内存寻址,这可以使代码运行更高效。

在最后一行中,因为Undefined Behavior指出空指针不能取值,所以编译器断定被取值的指针一定不为NULL,然后就可以对代码进行优化。



编译器如何优化代码

1306450-4885ce245448d8f2.png

编译器读入你的源代码,然后生成中间代码并进行分析和优化,最后输出二进制。



优化无用代码

1306450-32412bc7469e8e2e.png

无用代码:不执行或者不对执行结果造成影响的代码。
移除无用代码可以缩减应用的体积。

示例代码:

int foo(int *P) {
    int var = *P;
    return 42;
} 

消除无用代码后:

int foo(int *P) {
   return 42;
} 

所以,即使对该函数传入NULL,也不会发生错误。
因为该段代码无用,编译器已经将其消除。



优化顺序

void contains_null_check(int *P) {
    int unused = *P;
    ...
    if (P == NULL)
        return;
    *P = 4;
} 

对上面的代码进行优化时,可能有不同的优化方案。



1306450-16eeeb7b28cce6d8.png
方案一

1.消除多余的NULL检查:

void contains_null_check(int *P) {
    int unused = *P;
    ...
    *P = 4;
}

2.消除无用代码:

void contains_null_check(int *P) {
    ...
    *P = 4;
} 




1306450-94336020aee25c44.png
方案二

1.消除无用代码:

void contains_null_check(int *P) {
    ...
    if (P == NULL)
        return;
    *P = 4;
}

2.消除多余的NULL检查:

void contains_null_check(int *P) {
    ...
    if (P == NULL)
        return;
    *P = 4;
}

可以看出,不同的优化方案可以有差别极大的优化结果!




不同的编译器版本,可能有不同的优化方案。

在这些情况下,优化方案会发生变化:

  • 切换debug和release模式
  • 切换优化选项


    1306450-65d3340ca1bbf7ba.png
  • 切换不同的编译器
  • 切换模拟器和真机


    1306450-722d383e32821408.png
  • 切换Xcode的主要版本



安全性问题

  • 缓冲区溢出
  • 使用未初始化的变量
  • 使用释放后的变量
  • 重复释放
  • 多线程数据竞争



使用工具进行问题检测



Static Analyzer:

1306450-a6314255490a3713.png

使用建议:

  • 在每次build的时候运行
  • 在CI平台中运行



Runtime Sanitizers:

  • Address Sanitizer (缓冲区溢出、使用释放后的变量、重复释放)
  • Thread Sanitizer (数据竞争)
  • Undefined Behavior Sanitizer (未对齐的指针、对NULL指针取值、整数溢出、类型不匹配等)
1306450-a595a7d1a9e71ecf.png



在Scheme - diagnostics中开启:

1306450-92714caff05447e1.png



Swift 更安全

1306450-2847bfe44c4f94bc.png

使用Swift也会遭遇Undefined Behavior:

  • 需要和C语言家族的API交互
  • 使用UnsafePointer<T>, UnsafeMutableRawBufferPointer类型





参考内容:
Understanding Undefined Behavior




转载请注明出处,谢谢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值