UB 递增 NULL 指针导致的错误示例
Example of error caused by UB of incrementing a NULL pointer
此代码:
int *p = nullptr;
p++;
导致在
中讨论的未定义行为
但是在向其他人解释为什么他们应该避免使用 UB 时,除了说它不好因为 UB 意味着任何事情都可能发生之外,我还喜欢举一些例子来证明它。我有大量用于访问超出限制的数组的方法,但我找不到一个。
我什至试过了
int testptr(int *p) {
intptr_t ip;
int *p2 = p + 1;
ip = (intptr_t) p2;
if (p == nullptr) {
ip *= 2;
}
else {
ip *= -2;
} return (int) ip;
}
在一个单独的编译单元中希望优化编译器会跳过测试,因为当p
为空时,第int *p2 = p + 1;
行是UB,允许编译器假定代码不包含UB .
但是 gcc 4.8.2(我没有可用的 gcc 4.9)和 clang 3.4.1 都给出了正值!
有人可以建议一些更聪明的代码或其他优化编译器来在递增空指针时出现问题吗?
摘自http://c-faq.com/null/machexamp.html:
Q: Seriously, have any actual machines really used nonzero null
pointers, or different representations for pointers to different
types?
A: The Prime 50 series used segment 07777, offset 0 for the null
pointer, at least for PL/I. Later models used segment 0, offset 0 for
null pointers in C, necessitating new instructions such as TCNP (Test
C Null Pointer), evidently as a sop to [footnote] all the extant
poorly-written C code which made incorrect assumptions. Older,
word-addressed Prime machines were also notorious for requiring larger
byte pointers (char *
's) than word pointers (int *
's).
The Eclipse MV series from Data General has three architecturally
supported pointer formats (word, byte, and bit pointers), two of which
are used by C compilers: byte pointers for char *
and void *
, and word
pointers for everything else. For historical reasons during the
evolution of the 32-bit MV line from the 16-bit Nova line, word
pointers and byte pointers had the offset, indirection, and ring
protection bits in different places in the word. Passing a mismatched
pointer format to a function resulted in protection faults.
Eventually, the MV C compiler added many compatibility options to try
to deal with code that had pointer type mismatch errors.
Some Honeywell-Bull mainframes use the bit pattern 06000 for
(internal) null pointers.
The CDC Cyber 180 Series has 48-bit pointers consisting of a ring,
segment, and offset. Most users (in ring 11) have null pointers of
0xB00000000000. It was common on old CDC ones-complement machines to
use an all-one-bits word as a special flag for all kinds of data,
including invalid addresses.
The old HP 3000 series uses a different addressing scheme for byte
addresses than for word addresses; like several of the machines above
it therefore uses different representations for char *
and void *
pointers than for other pointers.
The Symbolics Lisp Machine, a tagged architecture, does not even have
conventional numeric pointers; it uses the pair <NIL, 0>
(basically a
nonexistent <object, offset> handle) as a C null pointer.
Depending on the ``memory model'' in use, 8086-family processors (PC
compatibles) may use 16-bit data pointers and 32-bit function
pointers, or vice versa.
Some 64-bit Cray machines represent int *
in the lower 48 bits of a
word; char *
additionally uses some of the upper 16 bits to indicate a
byte address within a word.
鉴于这些空指针在引用的机器中具有奇怪的位模式表示,您输入的代码:
int *p = nullptr;
p++;
不会给出大多数人期望的值 (0 + sizeof(*p)
)。
相反,您将拥有一个基于您的机器特定 nullptr
位模式的值(除非编译器有空指针算法的特殊情况,但由于标准没有强制要求,您很可能面对具有 "visible" 具体效果的未定义行为)。
这个例子怎么样:
int main(int argc, char* argv[])
{
int a[] = { 111, 222 };
int *p = (argc > 1) ? &a[0] : nullptr;
p++;
p--;
return (p == nullptr);
}
从表面上看,这段代码说:'如果有任何命令行参数,初始化 p
以指向 a[]
的第一个成员,否则将其初始化为空。然后递增它,然后递减它,然后告诉我它是否为空。'
从表面上看,如果我们提供命令行参数,这应该是 return '0'(表示 p
非空),如果我们提供命令行参数,则应该是 '1'(表示空)不。
请注意,我们绝不会取消引用 p
,如果我们提供参数,则 p
始终指向 a[]
.
的范围内
使用命令行编译 clang -S --std=c++11 -O2 nulltest.cpp
(Cygwin clang 3.5.1) 生成以下生成的代码:
.text
.def main;
.scl 2;
.type 32;
.endef
.globl main
.align 16, 0x90
main: # @main
.Ltmp0:
.seh_proc main
# BB#0:
pushq %rbp
.Ltmp1:
.seh_pushreg 5
movq %rsp, %rbp
.Ltmp2:
.seh_setframe 5, 0
.Ltmp3:
.seh_endprologue
callq __main
xorl %eax, %eax
popq %rbp
retq
.Leh_func_end0:
.Ltmp4:
.seh_endproc
此代码表示 'return 0'。它甚至懒得检查命令行参数的数量。
(有趣的是,注释掉减量对生成的代码没有影响。)
当不用于需要使用程序员知道有意义但编译器不知道的指针的各种系统编程时,理想的 C 实现将确保每个指针有效或可识别为无效, 并且会捕获任何时间代码试图取消引用无效指针(包括 null)或使用非法手段创建不是有效指针的东西 但可能被误认为是一个 。在大多数平台上,生成的代码在所有情况下都强制执行这样的约束会非常昂贵,但是防止许多常见的错误情况要便宜得多。
在许多平台上,让编译器为 *foo=23
生成等同于 if (!foo) NULL_POINTER_TRAP(); else *foo=23;
的代码相对便宜。即使是 1980 年代的原始编译器也经常有一个选项。但是,如果编译器允许空指针以不再被识别为空指针的方式递增,则此类陷阱的用处可能会大大降低。因此,一个好的编译器应该在启用错误捕获时将 foo++;
替换为 foo = (foo ? foo+1 : (NULL_POINTER_TRAP(),0));
。可以说,真正的 "billion dollar mistake" 并不是发明空指针,而是一些编译器会捕获直接空指针存储,但不会捕获空指针算术这一事实。
鉴于理想的编译器会陷入尝试递增空指针的尝试(许多编译器出于性能而非语义的原因而无法这样做),我看不出为什么代码应该期望这样的递增意义。在几乎任何情况下,程序员可能希望编译器为这样的构造分配含义[例如((char*)0)+5
产生指向地址 5] 的指针,程序员最好使用其他构造来形成所需的指针(例如 ((char*)5)
)。
这只是为了完成,但@HansPassant 在评论中提出的 link 确实值得被引用作为答案。
所有参考文献here,以下只是部分摘录
本文是关于 C 抽象的新的内存安全解释
为利益提供更强保护的机器
安全和调试 ... [作家] 证明内存安全实现是可能的
C 不仅支持 C 抽象机
如指定的那样,但更广泛的解释仍然兼容
与现有代码。通过在硬件中实施模型,
我们的实现提供了可以使用的内存安全
为 C 提供高级安全属性 ...
[实施] 内存能力表示为
松散包装的三元组(基本、绑定、权限)
转换成一个 256 位的值。这里 base 提供了一个虚拟的偏移量
地址区域,并绑定限制区域的大小
访问...特殊能力
加载和存储指令允许溢出功能
到堆栈或存储在数据结构中,就像指针一样......
警告指针减法是不允许的。
权限的添加允许功能
是向引用的内存授予某些权利的令牌。
例如,内存功能可能具有权限
读取数据和功能,但不写入它们(或只是
写入数据但不写入功能)。 正在尝试任何
不允许的操作将导致陷阱.
[]结果证实,保留强者是可能的
能力系统记忆模型的语义(它提供
不可绕过的内存保护)而不牺牲
低级语言的优点。
(强调我的)
这意味着即使它不是一个可操作的编译器,也存在构建一个可以捕获错误指针用法的研究,并且已经发布。
此代码:
int *p = nullptr;
p++;
导致在
但是在向其他人解释为什么他们应该避免使用 UB 时,除了说它不好因为 UB 意味着任何事情都可能发生之外,我还喜欢举一些例子来证明它。我有大量用于访问超出限制的数组的方法,但我找不到一个。
我什至试过了
int testptr(int *p) {
intptr_t ip;
int *p2 = p + 1;
ip = (intptr_t) p2;
if (p == nullptr) {
ip *= 2;
}
else {
ip *= -2;
} return (int) ip;
}
在一个单独的编译单元中希望优化编译器会跳过测试,因为当p
为空时,第int *p2 = p + 1;
行是UB,允许编译器假定代码不包含UB .
但是 gcc 4.8.2(我没有可用的 gcc 4.9)和 clang 3.4.1 都给出了正值!
有人可以建议一些更聪明的代码或其他优化编译器来在递增空指针时出现问题吗?
摘自http://c-faq.com/null/machexamp.html:
Q: Seriously, have any actual machines really used nonzero null pointers, or different representations for pointers to different types?
A: The Prime 50 series used segment 07777, offset 0 for the null pointer, at least for PL/I. Later models used segment 0, offset 0 for null pointers in C, necessitating new instructions such as TCNP (Test C Null Pointer), evidently as a sop to [footnote] all the extant poorly-written C code which made incorrect assumptions. Older, word-addressed Prime machines were also notorious for requiring larger byte pointers (
char *
's) than word pointers (int *
's).The Eclipse MV series from Data General has three architecturally supported pointer formats (word, byte, and bit pointers), two of which are used by C compilers: byte pointers for
char *
andvoid *
, and word pointers for everything else. For historical reasons during the evolution of the 32-bit MV line from the 16-bit Nova line, word pointers and byte pointers had the offset, indirection, and ring protection bits in different places in the word. Passing a mismatched pointer format to a function resulted in protection faults. Eventually, the MV C compiler added many compatibility options to try to deal with code that had pointer type mismatch errors.Some Honeywell-Bull mainframes use the bit pattern 06000 for (internal) null pointers.
The CDC Cyber 180 Series has 48-bit pointers consisting of a ring, segment, and offset. Most users (in ring 11) have null pointers of 0xB00000000000. It was common on old CDC ones-complement machines to use an all-one-bits word as a special flag for all kinds of data, including invalid addresses.
The old HP 3000 series uses a different addressing scheme for byte addresses than for word addresses; like several of the machines above it therefore uses different representations for
char *
andvoid *
pointers than for other pointers.The Symbolics Lisp Machine, a tagged architecture, does not even have conventional numeric pointers; it uses the pair
<NIL, 0>
(basically a nonexistent <object, offset> handle) as a C null pointer.Depending on the ``memory model'' in use, 8086-family processors (PC compatibles) may use 16-bit data pointers and 32-bit function pointers, or vice versa.
Some 64-bit Cray machines represent
int *
in the lower 48 bits of a word;char *
additionally uses some of the upper 16 bits to indicate a byte address within a word.
鉴于这些空指针在引用的机器中具有奇怪的位模式表示,您输入的代码:
int *p = nullptr;
p++;
不会给出大多数人期望的值 (0 + sizeof(*p)
)。
相反,您将拥有一个基于您的机器特定 nullptr
位模式的值(除非编译器有空指针算法的特殊情况,但由于标准没有强制要求,您很可能面对具有 "visible" 具体效果的未定义行为)。
这个例子怎么样:
int main(int argc, char* argv[])
{
int a[] = { 111, 222 };
int *p = (argc > 1) ? &a[0] : nullptr;
p++;
p--;
return (p == nullptr);
}
从表面上看,这段代码说:'如果有任何命令行参数,初始化 p
以指向 a[]
的第一个成员,否则将其初始化为空。然后递增它,然后递减它,然后告诉我它是否为空。'
从表面上看,如果我们提供命令行参数,这应该是 return '0'(表示 p
非空),如果我们提供命令行参数,则应该是 '1'(表示空)不。
请注意,我们绝不会取消引用 p
,如果我们提供参数,则 p
始终指向 a[]
.
使用命令行编译 clang -S --std=c++11 -O2 nulltest.cpp
(Cygwin clang 3.5.1) 生成以下生成的代码:
.text
.def main;
.scl 2;
.type 32;
.endef
.globl main
.align 16, 0x90
main: # @main
.Ltmp0:
.seh_proc main
# BB#0:
pushq %rbp
.Ltmp1:
.seh_pushreg 5
movq %rsp, %rbp
.Ltmp2:
.seh_setframe 5, 0
.Ltmp3:
.seh_endprologue
callq __main
xorl %eax, %eax
popq %rbp
retq
.Leh_func_end0:
.Ltmp4:
.seh_endproc
此代码表示 'return 0'。它甚至懒得检查命令行参数的数量。
(有趣的是,注释掉减量对生成的代码没有影响。)
当不用于需要使用程序员知道有意义但编译器不知道的指针的各种系统编程时,理想的 C 实现将确保每个指针有效或可识别为无效, 并且会捕获任何时间代码试图取消引用无效指针(包括 null)或使用非法手段创建不是有效指针的东西 但可能被误认为是一个 。在大多数平台上,生成的代码在所有情况下都强制执行这样的约束会非常昂贵,但是防止许多常见的错误情况要便宜得多。
在许多平台上,让编译器为 *foo=23
生成等同于 if (!foo) NULL_POINTER_TRAP(); else *foo=23;
的代码相对便宜。即使是 1980 年代的原始编译器也经常有一个选项。但是,如果编译器允许空指针以不再被识别为空指针的方式递增,则此类陷阱的用处可能会大大降低。因此,一个好的编译器应该在启用错误捕获时将 foo++;
替换为 foo = (foo ? foo+1 : (NULL_POINTER_TRAP(),0));
。可以说,真正的 "billion dollar mistake" 并不是发明空指针,而是一些编译器会捕获直接空指针存储,但不会捕获空指针算术这一事实。
鉴于理想的编译器会陷入尝试递增空指针的尝试(许多编译器出于性能而非语义的原因而无法这样做),我看不出为什么代码应该期望这样的递增意义。在几乎任何情况下,程序员可能希望编译器为这样的构造分配含义[例如((char*)0)+5
产生指向地址 5] 的指针,程序员最好使用其他构造来形成所需的指针(例如 ((char*)5)
)。
这只是为了完成,但@HansPassant 在评论中提出的 link 确实值得被引用作为答案。
所有参考文献here,以下只是部分摘录
本文是关于 C 抽象的新的内存安全解释 为利益提供更强保护的机器 安全和调试 ... [作家] 证明内存安全实现是可能的 C 不仅支持 C 抽象机 如指定的那样,但更广泛的解释仍然兼容 与现有代码。通过在硬件中实施模型, 我们的实现提供了可以使用的内存安全 为 C 提供高级安全属性 ...
[实施] 内存能力表示为 松散包装的三元组(基本、绑定、权限) 转换成一个 256 位的值。这里 base 提供了一个虚拟的偏移量 地址区域,并绑定限制区域的大小 访问...特殊能力 加载和存储指令允许溢出功能 到堆栈或存储在数据结构中,就像指针一样...... 警告指针减法是不允许的。
权限的添加允许功能 是向引用的内存授予某些权利的令牌。 例如,内存功能可能具有权限 读取数据和功能,但不写入它们(或只是 写入数据但不写入功能)。 正在尝试任何 不允许的操作将导致陷阱.
[]结果证实,保留强者是可能的 能力系统记忆模型的语义(它提供 不可绕过的内存保护)而不牺牲 低级语言的优点。
(强调我的)
这意味着即使它不是一个可操作的编译器,也存在构建一个可以捕获错误指针用法的研究,并且已经发布。