ub行为未定义导致的问题(指针越界 大小端)
ub (Undefined Behavior)未定义行为
- 什么是[UB]
LLVM IR和C语言中都有UB的概念。很多在C语言中看似合理的事都可能导致UB,UB是代码中很多BUG的源泉。
- UB的优点
- 使用未初始化的变量:很多C程序BUG的根源,编译器等很多工具都可以轻松找出这种问题。
但C程序中不像JAVA等语言对变量进行清零初始化,也带来了性能的收益,特别是对栈上的数组,malloc申请的内存等需要调用memset的场景。
- 有符号整型溢出:如果有符号整型的算数运算导致溢出,那么结果是未定义的。一个例子是INT_MAX+1并不一定等于INT_MIN。
这个UB对某些特定的优化非常重要,比如"X + 1 > X"一定为true,“X * 2 / 2” 一定等于X。
一些更重要的优化,比如如下的循环:
for (i = 0; i <= N; ++i) { ... }
编译器可以假定这个循环一定迭代N+1次,这样就可以应用很多循环优化,提升循环的性能。
Clang和GCC都提供了"-fwrapv"选项强制编译器将有符号整型溢出变为确定的行为(Defined Behavior),这样自然就会丢掉这些优化机会。
- 移位溢出:对一个uint32_t移位超过32bit的结果是未定义的。
原因是因为不同的CPU的移位实现差异很大,比如x86会将移位的位数截断到5bit表示的范围(移位32bit = 移位0bit)。
但是PowerPC将移位的位数截断到6bit表示的范围(移位32bit = 产生数字0)。
编译器如果要消除这类UB,需要增加一条额外的指令,比如and,会带来性能损失。
- 解引用野指针和数组访问越界:解引用野指针(空指针、指向已经free的内存等)和数组访问越界是C程序中的常见BUG。
要消除这类UB,每次数组访问需要增加越界检查,并且ABI也需要保证指针对应的边界信息被保存下来,用于检查指针的数值算术运算。
- 解引用空指针:解引用空指针是未定义行为,意味着就算你mmap了一个page到0地址,也不保证解引用空指针会访问到这个page。
在C系列语言中,解引用空指针的未定义给编译器提供了一系列的优化机会,表现为宏展开和inline中的优化等。
- 违反类型规则:比如将int转换为float,再进行解引用,是未定义行为。
这个UB为编译器做基于类型的别名分析(TBAA)提供了帮助。
比如clang可以直接把下列代码:
float *P; void zero_array() { int i; for (i = 0; i < 10000; ++i) P[i] = 0.0f; }
变为:
memset(P, 0, 40000)
可以通过-fno-strict-aliasing来关闭这个UB,这样会导致上面的代码不能被优化为memset。
因为编译器不能保证对P[i]的赋值不会改变P本身,比如如下代码:
int main() { P = (float*)&P; // cast causes TBAA violation in zero_array. zero_array(); }
当然上面这样的类型滥用是不常见的,所以标准委员会决定将违反类型规则定义为UB,保证更好的性能。
值得注意的是JAVA没有这样的问题,因为JAVA没有不安全的指针类型转换。
整体来说,除了由于优化导致的UB,还有一些其他的类型,比如序列点违规:“foo(i, i++)”,多线程竞争条件,restrict违规,除0等。
(42条消息) C程序中的未定义行为(Undefined Behavior)_c语言未定义行为_vincent&lin的博客-CSDN博客
指针指向数组越界的地方
如:
int a[]={1,2,3};
int* p=&a[3];
行为未定义,而且这个未定义比很多人想的都要强很多
实际情况是,C语言规定你只能指向一个数组元素__下标[0, size]的范围__才是正常代码,注意右边是__闭区间__,例如你问的这个是__int a[3],那么你给它__赋值&a[0]到&a[3]都是合法的,虽然__&a[3]的存取是未定义行为__,但是指向a[3]的指针值__是合法的,不会产生未定义行为__,然而,&a[-1]、&a[4]这些都是非法代码,即便你只赋值不存取
链接:https://www.zhihu.com/question/354594959/answer/887210440
大小端序
大端序:高字节保存在内存的低地址
小端序:高字节保存在内存的高地址
那么怎么才能知道自己的设备使用什么方式存储的数据呢?
很简单,用 memcpy
把数据对应的内存拷贝出来,人工检查下就好了。
const int MAX_LEN = 32;
void checkram(char *host, void *sth)
{
memcpy(host, sth, sizeof(sth));
for (int i = 0; i < MAX_LEN; i++)
{
if (i % 8 == 0) puts("");
printf(" %02x", (unsigned char)host[i]);
}
puts("********");
}
int a = 1;
long long b = -2;
char ram1[MAX_LEN], ram2[MAX_LEN];
checkram(ram1, &a);
checkram(ram2, &b);
我们都知道 int
类型的 1
实际上是 0x0000 0001
,long long
类型的 -2
的补码表示是 0xffff ffff ffff fffe
,所以只要看一下关键字 01
和 fe
是否出现在第一个内存字节上,就可以判断是大端序还是小端序了。我的运行环境是 x86_64 给出的结果是这样的。
01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
********
fe ff ff ff ff ff ff ff
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
********
这明显是小端序了。事实上,x86 平台一般都是用小端序来储存的,所以知道了平台的架构,也就知道了内存的储存数据时的结构。
C++ 杂谈别把 ub 当作知识点
C++ 杂谈(一)别把 ub 当作知识点 - 知乎 (zhihu.com)
一处核心 ub,那就是关于 union 的初始化
问题。一开始我的理解是 union 不允许内部成员拥有构造函数。但是实际上这个理解是错误的。正确的理解是,通过初始化共用体union 其中的一个成员试图来完成其他成员的初始化的行为本身,就是一个未定义行为。而之所以代码能够运行,是因为本地的编译器允许了这种操作。那么既然如此,这个题目本身就是一个 ub,所以随后的讨论自然也属于一种“瞎猫抓到了死耗子”的碰巧行为。