介绍C语言中的Undefined Behaviors-part1 - lethelog

介绍C语言中的Undefined Behaviors-part1

wliao posted @ Sat, 14 May 2011 22:32:54 +0800 in 未分类 , 4498 readers

 

在阅读C standard以及Cpp standard的过程中我们经常可以发现标准中并没有给出明确的行为,取而代之的是//the behavior is undefined.

比如说:

Anything at all can happen; the Standard imposes no requirements. The program may fail to compile, or it may execute incorrectly (either crashing or silently generating incorrect results), or it may fortuitously do exactly what the programmer intended.

From C FAQ

今天看到LLVM blog上有一篇关于C语言中的undefined behavior的介绍,里面推荐了另外一篇文章A Guide to Undefined Behaviors in C and C++

这里简单翻译一些后面那篇文章,作为对这方面的一个总结吧。

Undefined Behavior模型

暂时忽略编译器,只考虑C语言的实现,这种实现符合C语言标准(行为与C虚拟机的行为一致)。如果执行一个程序的过程中,每一步行为都是明确定义的,那么整个过程都是明确定义的。不过,注意就算如此,过程的行为仍然不是唯一的,这里又设计到了未特殊化以及实现确立的行为(在这里忽略)。

如果程序执行过程中有任何一部存在undefined行为,那么整个执行都是“无意义”的。

看一个简单的例子:

#include <limits.h>
#include <stdio.h>
int main (void)
{
printf ("%d\n", (INT_MAX+1) < 0);
return 0;
}

这个程序需要C实现解决一个简单的问题,如果我们在最大可表示出来的整型上加一,是否结果为负数?

$ cc test.c -o test
$ ./test
1

可能得到这样的结果:

$ cc test.c -o test
$ ./test
0

也有可能是:

$ cc test.c -o test
$ ./test
42

还有可能是:

$ cc test.c -o test
$ ./test
Formatting root partition, chomp chomp
可能有人会说编译器行为是不正确的,因为在C标准明确指出关系操作符返回的必须是1或者0.但是因为这里的程序“没有意义”,所以实现时可以做任何编译器想做的事情。
Undefined behavior扰乱了C虚拟机中的所有其他行为。当然最后一个可能性几乎是为零的,但是这些undefined behavior经常导致安全漏洞。
人们常常会说,至少这么认为:
x86下ADD汇编指令是用来实现C中的带符号加法的,那么当结果溢出时,这个指令有两种互补的行为。我现在正在X86下变成,那么至少我能肯定溢出时会出现其中一种语义结果。
这样说是不对的,正如你说:
有些人告诉我在篮球比赛中,可以抱住球然后跑动。我又一个篮球,并且尝试是可以的。明显他是理解篮球比赛规则的。
其实这中情况有点微妙:
  1. 有任何C语言实现形式保证实现了这两种互补的行为么?当然有。许多编译器在优化选项关闭时会实现这种行为,如gcc中有一个(-fwrapv)选项用来在任何优化级别下强制使用这种行为。其他编译器默认强制使用。
  2. 然而,同样也有某些编译器并没有实现这两种互补行为。
  3. 更有甚者,有些编译器(比如GCC)整型溢出的行文已经存在多年,优化器变得更聪明了,导致与整型溢出突然静悄悄的不像想象中那样工作了。

这些当然对于标准来说都是可以接受的,但是对开发者来说这是一种噩梦。不过,这对编译器小组来说在跑分方面是一种胜利。

Undefined Behavior的优缺点:

优点:

  1. 唯一的优点就是简化了编译器的工作。让它在某些情况下产生非常有效率的代码。通常这些情况包含紧循环。

缺点:

  1. 当程序员对可靠的避免undefined behavior没有信心的时候,通常会造成行为异常的程序。
  2. 另外一个不太严重的问题是,或者说是困扰的是,undefined behavior仅仅是为了给编译器小组编写减轻负担,而不是为了获得更高的效率。
从编译器的角度来理解Undefined Behavior

一种关键的深入理解含有Undefined Behavior的语言的方式是编译器只需要尽职尽责的考虑并且完成明确定义的行为。

如果我们假象一个C程序正在被C虚拟机执行,undefined behavior是非常容易理解的:程序中的每个操作不是defined的就是undefined的,并且通常很容易区分。当我们开始关系程序所有可能的执行方式的时候,undefined behavior变得很难处理。程序开发者,通常需要在每种情况下都获得正确结果,因此很在意这些可能性,同样需要在任何可能情况下都生成正确的机器码的编译器开发者也如此。

下面讨论一个程序的所有可能执行方式,首先我们对其进行一定简化,加入如下的一些条件:

  • 仅讨论C/C++函数,而不是整个程序
  • 假设对任何输入,函数都能结束
  • 假设函数的执行是确定性的,如并不和其他线程通过共享内存交互
  • 假设我们有无限的资源来完整测试函数,这里的完整测试指的是所有可能的输入都测试。完整测试枚举所有的输入,从最小的输入(根据比特位衡量)开始,测试所有符合输入定义的可能的比特组合。

根据这个测试我们可以得到始终情况:

  1. 对于所有输入,行为都是确定的
  2. 对于所有输入,某些是确定的,某些不是
  3. 对于所有输入,所有都是不确定的
情况一:

对输入无限制,行为都是确定的。通常API级别的函数以及那些处理“未净化数据”的函数应该满足这一类。

例,处理整型除法的应用函数:

int32_t safe_div_int32_t (int32_t a, int32_t b) {
  if ((b == 0) || ((a == INT32_MIN) && (b == -1))) {
    report_integer_math_error();
    return 0;
  } else {
    return a / b;
  }
}
情况三:

这类函数没有明确定义的执行方式。严格来说,这些函数是“无意义“的:编译器甚至不被要求生成一个return标志。那么这种类型真的存在么?是的,而且很常见。

比如说无论输入是什么的时候,函数中只用了一个并没初始化的变量时(这通常在无意间写出)。编译器在识别以及利用这种代码方面变得越来越智能。下面是Google Native Client project中的代码:

When returning from trusted to untrusted code, we must sanitize the return address before taking it. This ensures that untrusted code cannot use the syscall interface to vector execution to an arbitrary address. This role is entrusted to the function NaClSandboxAddr, in sel_ldr.h. Unfortunately, since r572, this function has been a no-op on x86.

-- What happened?

During a routine refactoring, code that once read

aligned_tramp_ret = tramp_ret & ~(nap->align_boundary - 1);

was changed to read

return addr & ~(uintptr_t)((1 << nap->align_boundary) - 1);

Besides the variable renames (which were intentional and correct), a shift was introduced, treating nap->align_boundary as the log2 of bundle size.

We didn't notice this because NaCl on x86 uses a 32-byte bundle size.  On x86 with gcc, (1 << 32) == 1. (I believe the standard leaves this behavior undefined, but I'm rusty.) Thus, the entire sandboxing sequence became a no-op.

This change had four listed reviewers and was explicitly LGTM'd by two. Nobody appears to have noticed the change.

-- Impact

There is a potential for untrusted code on 32-bit x86 to unalign its instruction stream by constructing a return address and making a syscall. This could subvert the validator. A similar vulnerability may affect x86- 64.

ARM is not affected for historical reasons: the ARM implementation masks the untrusted return address using a different method.

到底发生了什么?一个简单的重构将包含这个代码的函数变为了类型三。报告这个问题的人认为x86下gcc会将(1<<32)解释为1,但是这里并没有任何理由确认这个行为是可靠的(实际上一些我试过的x86-gcc并没有这么做)。这样的构造理所当然的是undefined因此编译器可以做任何它想做的事。对于典型的C编译器,并不会产生对应于这个undefined操作的指令(C编译器的首要任务是产生高效率的代码)。

类型二

对于某些输入有确定行为,而对其他的没有。这也是我们要讨论的最有趣的部分。

简单的带符号除法就是一个很好的例子:

int32_t unsafe_div_int32_t (int32_t a, int32_t b) {
  return a / b;
}
这个函数存在前提条件,因为它只能被满足如下条件的参数调用。
(b != 0) && (!((a == INT32_MIN) && (b == -1)))
与类型一的判决条件相似当然不是一个意外。如果调用方违反了这个先决条件,你的程序的含义也遭到破坏。
那么编写这样的,没有细节先决判别条件的函数是否好呢?总体上来说,对于内部使用,这是可以的,只需要有文档指出这种条件的存在。
现在,让我们来看看编译器将这函数编译为object code的工作。编译器执行一个分析:
  1. 在如上条件满足的情况下,编译器有责任生成计算a/b的指令
  2. 不满足时,编译器并无任何责任

编译器编写者会问自己这样一个问题:最有效的实现下面这两种情形的方法是什么?因为第二种情况并无编译器任何责任产生正确代码,所以最简单的事就是不要考虑它。编译器可以只对第一种情况生成指令。(在Java编译器中,则相反,Java编译器有责任解决第二种情况,虽然在这里是一种特殊情况,程序并不需要运行时的开销,因为预编译器通常可以在除零时提供自陷行为)

在看一个类型二的函数:

int stupid (int a) {
  return (a+1) > a;
}
先决条件是:
(a != INT_MAX)
通常好的编译器只会产生这样的汇编代码:
stupid:
  movl $1, %eax
  ret
但是如果在这儿我们使用-fwrapv标志,告诉GCC,整型溢出有两种互补行为,我们会得到不同的分析结果:
  1. a != INT_MAX:有责任返回1
  2. a == INT_MAX: 有责任返回0

这样编译器必须生成需要计算而且检查结果的代码:

stupid:
  leal 1(%rdi), %eax
  cmpl %edi, %eax
  setg %al
  movzbl %al, %eax
  ret
同样,Java也需要做同样的操作。
_ZN13HelloWorldApp6stupidEJbii:
  leal 1(%rsi), %eax
  cmpl %eax, %esi
  setl %al
  ret
只需要牢记,编译器的任务就是提供给你满足要求的快速的代码,所以他们会试着忘记undefined behavior,以能够获得最快的速度,然而并不会告诉你到底发生了什么。
一个有趣的分析例子

大约一年前,Linux内核开始使用一个特殊的GCC标识用来告诉编译器避免优化掉无用的空指针。原因代码如下(有修改):

static void __devexit agnx_pci_remove (struct pci_dev *pdev)
{
  struct ieee80211_hw *dev = pci_get_drvdata(pdev);
  struct agnx_priv *priv = dev->priv; 

  if (!dev) return;
  ... do stuff using dev ...
}
这段代码是为了获取一个device结构体的指针,检查是否为空,然后使用。
但是这里有一个问题!在这个函数中,这个指针在检查是否为空之前就已经进行了引用。
这导致会产生优化的编译器进行下面的分析:
  1. dev == NULL:无责任
  2. dev != NULL:检查是否为空,一定返回否。因此,此判断语句无意义,可能会被删除。

在两种情况下,判空语句都是无用的,检查语句被删除,造成了安全漏洞的隐患。

当然这里的问题在于在未检查情况下就引用了指针所指向的结构体,所以只需要将检查语句放到上面就可以。不过这得需要这种语句被人工或者使用工具发现,所以如果告诉编译器一些保守方式更好。

有undefined behavior的生活

长期的,不安全的编程语言并不会被mainstream开发者所使用,而是仅将其应用在需要高性能以及低资源的情况下。同时,解决undefined behavior不是完全的直接所以patchwork方法似乎是最好的方法:

  • 开启编译器警告,最好使用多种编译器
  • 使用静态分析器(比如说Clang's,Coverity,etc..)来获取更多警告
  • 使用编译器支持的动态检查,如gcc的-ftrapv标志产生能够使带符号整型溢出时自陷
  • 使用类似Valgrind来获得更多的动态检查
  • 当函数是类型二时,使用文档记录他们的前提条件以及后续条件
  • 使用断言来确认函数的前提条件以及后续条件满足
  • 使用高质量的数据结构库(特别是在C++中)

基本的:万分小心,使用优秀的工具,尽力做到最好。

  • No match
asma said:
Sat, 21 Sep 2019 16:25:16 +0800

looking very<a href="http://www.alrouahcompany.com">product marketing</a>good

<a href="http://www. said:
Sat, 21 Sep 2019 16:25:55 +0800

looking very good

cleaning services ab said:
Mon, 14 Oct 2019 14:31:21 +0800

Currently all sorts of cleaning companies like business office, carpet, settee, curtain, dried up cleaning, deep washing in Dubai and even more. We value customer care to the best level and offer consistent washing in Dubai. Currently customized slated cleaning companies in Dubai to your customers.

shaheed diwas status said:
Tue, 17 Mar 2020 14:40:06 +0800

download video songs for upcoming event 23 march shaheed diwas

cleaning services du said:
Thu, 16 Sep 2021 14:54:15 +0800

Any foremost thing which are required is that will vacuum or simply sweep earliest. Do the without standard water. Sweep off most of the dirt so that it can avert from appearing sticky in the grass or appearing muddy. Begin with the four corners and work from a systematic strategy. If you now have a vacuum clearer, then feel free to use it. In any other case, a broomstick is completely fine. Broomsticks could vary according towards your floor.

maid services dubai said:
Wed, 29 Sep 2021 16:47:32 +0800

Folks who own their own individual companies gotten away through skimping at janitorial and even professional purifier services for ages. Companies in the uk used you need to do their possess dirty get the job done having home office managers retain the floors wash and secretaries pay attention to the dusting. Professional detergents were reserved in the very sizeable companies which will owned skyscrapers with the city. Very never did so you see a folks and appear shop hire at a janitor, house maid service and professional internet business cleaning folks.

cleaning services said:
Wed, 29 Sep 2021 16:57:52 +0800

Housekeeping will always be of operate, but being on afford the several things essential for cleaning your entire household can be harder. Hiring trusted cleaning companies is often a wise choice if you find money for you to spare, but in case you are looking to make ends meet and barely have a very dime pertaining to extra charges, even receiving affordable per hour maids' solutions may be a wrestle. Fortunately, hiring vacation professionals to scrub your household is just not compulsory, not is employing commercial products an necessary option.

full time maids in d said:
Wed, 30 Aug 2023 16:35:35 +0800

Don't you buy the cheaper tools, food items, clothing or everthing else? As a reliable painting professional, you realise that buying low-priced tools can often mean you possibly invest in a tool twofold, once should the cheap resource breaks as well as second time while you buy an effective tool to restore the low-priced tool this broke. Is cheap the perfect value? The consumer thinks in regards to this when checking out a dwelling painting approximation.


Login *


loading captcha image...
(type the code from the image)
or Ctrl+Enter
Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee