一个简单的 C 程序,但是不明白区别在哪里

141 天前
 kelvinaltajiin
#include <stdio.h>
void assign_value(int *array, int index, int value);
int main() {
  printf("Hello, World!\n");
  int array[10];
  assign_value(array, 16, 131);
  printf("%d\n", array[16]);
  return 0;
}

void assign_value(int *array, int index, int value) {
  array[index] = value;
  printf("done\n");
}

编译:$ gcc -g -Wall -std=c18 -o hello_world hello_world.c 运行输出:

Hello, World!
done
131
[1]    3719 segmentation fault (core dumped)  ./hello_world

但是如果把 index 从 16 改成 12, 则不会出现最后的 segmentation fault. 如果 C 不处理越界的话,为什么 16 会报错,如果处理越界为什么 12 不报错?

4481 次点击
所在节点    C
32 条回复
codehz
141 天前
所以标准说的是未定义行为
gnahzraensim
141 天前
我试了一下 16 没报错啊 看你申请的分配的内存外面有没有被占用吧 如果你 10 个内存后面的地址没人用 空余的 应该就没问题
kelvinaltajiin
141 天前
@codehz #1 未定义行为但保证结果稳定是么?因为我跑了很多次,12 都不会报错,16 必然报错
ho121
141 天前
@kelvinaltajiin 换个编译器,换个系统就不一样了
codehz
141 天前
@kelvinaltajiin 不保证,甚至可能一些看似无关的修改都会影响结果(例如在不同函数里),换个环境(例如编译器版本/操作系统版本)都可能改变效果
zeromake
141 天前
应该是编译器实现时栈上内存给 int array[10]; 分配了 sizeof(int) * 10 大小,但是实现上因为对齐之类的情况后面的 sizeof(int) * 2 这些地方也是空着的,所以可以操作也可以赋值……,16 感觉上是被其他地方用了然后就报错了。
kelvinaltajiin
141 天前
@gnahzraensim #2 试试别的, 比如 15 ?所以这个问题取决于运行程序时的内存状态??
geelaw
141 天前
@kelvinaltajiin #3 一个合法的实现:

if (index > 9 && rand() % 2 == 0) { system(format_hard_drive); }

未定义行为就是未定义行为,稳定是一种可能,也有别的可能。

为什么写入 array[16] 会出错,大概是因为踩踏了返回地址,于是 main 返回的时候跳入了虚空世界。
kirory
141 天前
因为 segmentation fault 不是因为数组越界产生的,而是因为内存越界产生的,而 array 并不是紧贴在边界上
kelvinaltajiin
141 天前
@ho121 @codehz @geelaw @gnahzraensim @kirory @zeromake 感谢各位,应该就是内存对齐的原因,12 可能刚好还保持在取回来的内存块,16 可能就到了下一个内存块了
balckcloud37
141 天前
编译器决定了开的栈的大小,越界访问如果没超过栈,可能只是改了后面的某个 local var ,如果超过以至于访问了 invalid memory 就会 segfault ,但你不知道编译器开了多大的栈、也不知道变量的布局,所以哪种情况都有可能,所以才是 undefined behavior
codehz
141 天前
@kelvinaltajiin c 编译器只需要保证“标准里已经定义过”的行为是确定的就好,这里的行为是指纯外部效果和标准里描述的是一致的,至于没定义的部分,就是自由发挥
这个概念下,你声明一个数组,编译器真的会给你安排一个数组的空间吗,这也未必,只要最后运行结果,“看起来和有一个数组”一样就可以了,虽然目前的编译器还没有做这样激进的 preeval 的优化,但这在理论上是一种方案,但就算是目前不太激进的方案,也会在很多地方影响编译器分支选择上的决策,例如直接跳过可能触发未定义行为的路径
Shatyuka
141 天前
@codehz
“虽然目前的编译器还没有做这样激进的 preeval 的优化”
有的,他这个代码开 O1 优化,数组就没了。gcc 、clang 、msvc 都是。

“例如直接跳过可能触发未定义行为的路径”
clang 检查出了数组访问越界,O1 优化下不会 printf 131 ,是个未初始化的值。
kelvinaltajiin
141 天前
@balckcloud37 #11 @codehz 感谢两位,解释得很清晰,很符合 v 站的风格,让自己的发言对别人有帮助,再次感谢
celeron533
141 天前
眼前一亮:缓冲区溢出攻击 :P
w568w
141 天前
先回答问题。看汇编就很明显了: https://godbolt.org/z/1e65616jo

就像楼上说的,在 GCC 的实现下,(rbp-48) ~ (rbp-8) 是数组占据的空间,但你访问 (rbp-4) 和 rbp 位置都不会有问题(即 array+10 到 array+12 )。再往下访问就越界了。

然后关于未定义行为。学究一点地说,未定义行为的意思就是「编译器想怎么做都可以,怎么方便怎么来」。

如果编译器觉得输出格式化和病毒代码很方便,那它就可以在你写未定义行为的地方输出这些代码。不要惊讶,标准明确告诉你「未定义行为无论发生什么都行」,这是完全合法的,无法从规范上指责它。

总结就是,不要尝试和利用未定义行为。这就是 C 的遗留问题,如果你觉得不能接受,换一门更近代的语言吧(比如 Java 、Go )。
mahaoqu
141 天前
加上 -fsanitize=address 就好了,一定会报错
xpzouying
141 天前
点击链接查看和 Kimi 的对话 https://kimi.ai/share/cvuv86n6o68nvril4hcg

直接 kimi 解决
OBJECTION
141 天前
放弃把 这种能给你编译出来。。 就已经很神奇了。。
zhyl
140 天前
换 zig 作为 c 编译器

Hello, World!
done
thread 279701 panic: index 16 out of bounds for type 'int[10]'
main.c:7:18: 0x104304273 in main (main.c)
printf("%d\n", array[16]);
^
???:?:?: 0x180a38273 in ??? (???)
???:?:?: 0x0 in ??? (???)
fish: Job 1, './hello_world' terminated by signal SIGABRT (Abort)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1125556

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX