C 小白一个,求各位大佬关照。问题代码见下:
#include <stdio.h>
int main() {
long i;
long j;
char *ch; // 这里的确是需要初始化
scanf("%s", ch);
i = 0; // 如果把这行注释掉,程序不会报错
j = 0; //
}
执行结果:
root@ubuntu:~# ./a.out
v2ex
Segmentation fault
但是把下面给i
和j
赋值的语句注释掉,或者只注释其中一条,却不会报错了:
root@ubuntu:~# ./a.out
v2ex
我是用objdump -d main.o
查看反汇编的代码:
0000000000400546 <main>:
400546: 48 83 ec 08 sub $0x8,%rsp
40054a: be 00 00 00 00 mov $0x0,%esi
40054f: bf f4 05 40 00 mov $0x4005f4,%edi
400554: b8 00 00 00 00 mov $0x0,%eax
400559: e8 d2 fe ff ff callq 400430 <__isoc99_scanf@plt>
40055e: b8 00 00 00 00 mov $0x0,%eax
400563: 48 83 c4 08 add $0x8,%rsp
400567: c3 retq
400568: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
发现scanf
的ch
指针的地址赋值为0x0
,这里的确是有问题,但是我想不通scanf
下面的语句为什么会影响到程序的执行。
求各位大佬指教啊。。。
1
lance6716276 2018-03-22 12:55:19 +08:00 via Android
先把代码写标准了再去 debug
|
2
shuax 2018-03-22 12:57:03 +08:00
char *ch 只是一个指针,都没有给他固定空间,然后就越界了。
|
3
yinanc 2018-03-22 13:01:12 +08:00
赋值给未分配空间的指针时情况就不可控了,后面发生什么都有可能
|
4
shsdust 2018-03-22 13:06:59 +08:00
不是指明了 Segmentation fault 吗?表示 CPU 试图访问无法定址的块,明显是因为指针没被分配空间,既然到这里已经出现了这种错误,下面的程序报错很正常的
|
5
shsdust 2018-03-22 13:07:39 +08:00
还有,编译是一门玄学
|
6
mrzys OP @shsdust 但是这样并不会报错:
``` #include <stdio.h> int main() { long i; long j; char *ch; scanf("%s", ch); i = 0; //j = 0; } ~ ``` 这就是令人奇怪的地方。` scanf`后面的代码会影响到程序会不会发生异常退出。 |
7
jiutai21 2018-03-22 13:19:27 +08:00
未定义的行为把,换个平台 /编译器结果不一定的
|
8
tomychen 2018-03-22 13:20:09 +08:00
|
9
liuzhedash 2018-03-22 14:14:05 +08:00
@mrzys #6
兄弟,这样写是否报错并不是你想的那种因果关系,建议阅读《深入理解计算机系统》的第三章。 |
10
doun 2018-03-22 14:17:14 +08:00 via Android
大概因为 scanf 下面的 int i,j 编译后,也是挪到栈上的吧
|
11
mrzys OP @liuzhedash 就在阅读 csapp,但是对于内存这一块还是很模糊。第 7 章 linker 中说了局部变量是在执行的时候在 stack 上动态分配的,但是我反汇编之后,查看不到这段代码中的 i 和 j 的赋值指令。只有`sub $0x8,%rsp`,栈指针向下增加了 8 个字节,函数执行完毕返回之前执行`add $0x8,%rsp`,栈指针向上移动了 8 个字节,栈指针完全没有被影响。
|
12
lingdux 2018-03-22 14:52:36 +08:00
那里的野指针需要申请空间,大小要足够容纳输入的内容。
注释掉那句不发生异常只是堆栈压栈出栈的巧合而已,程序并没有按照你的预期执行。 这种问题动态调式,注意一下堆栈和内存,一目了然。 7 楼不懂别瞎说误导新人。 |
16
lingdux 2018-03-22 15:09:52 +08:00 1
@mrzys
这个问题让我想起来《 0day 安全:软件漏洞分析技术》里面一个例子 虽然那个是 win 的,但是一样的原理。 2 . 2 修改邻接变量中,修改了相邻的变量,填补了被破坏的堆栈,导致程序改变了流程实现了破解 CrackMe。 |
17
pkookp8 2018-03-22 15:10:58 +08:00 via Android
尝试了 x86 和 arm,都不会出错
gcc5.3.1 armgcc4.4.1 编译方式 $(CC) src.c -o err.bin 不加其他编译参数 |
21
mengyaoss77 2018-03-22 15:50:31 +08:00
说是小白,玩的东西完全不像小白啊,直接反汇编。
反汇编看不懂,我只知道这个代码是危险的。就像 gets()一样 |
23
neoblackcap 2018-03-22 16:11:51 +08:00 via iPhone 1
为什么要研究 undefined behavior?这个发生什么事系统都是不作保证的
|
24
pkookp8 2018-03-22 16:16:05 +08:00 via Android
|
26
dummytaurus 2018-03-22 17:35:40 +08:00
这个只是巧合。进入 main 的时候,栈上第一个变量值是 nil,第二个指向栈上,第三个指向错误地址。所以 scanf 处理第三个变量会 segfault,而处理第一个(scanf 应该是有对 nil 做特殊处理)和第二个变量值没有问题。
注释前,gcc 无法优化,所以 ch 是第三个变量,指向错误地址。注释后,gcc 将优化掉 i 或者 j,不管怎样,ch 会变成第一个或者第二个变量,它的值都不会引起 scanf 崩溃。 不过栈上的初始值肯定是和 crt 相关的,macOS 上面两个示例都会蹦 |
28
mrzys OP @dummytaurus 我使用 gdb 打印了一下未初始化的指针的值,两次指针指向的地址不一样,没注释 i 和 j 的时候指针地址是合法的,注释其中一个的时候指针地址指向了 read-only code segment。的确和 crt 有关系。我怀疑是调用 main 之前已经使用了栈空间,栈上的值被上次的函数调用写入了数据,当调用 main 的时候,因为指针没有初始化,用的上次的值。
|
29
mrzys OP @pkookp8 我自己测试环境是 ubuntu x86-64,用 macOS 也测试了一下,-O0 的时候会报错,但是-O1 的时候不会报错。
|
30
mrzys OP @lingdux 大佬,我动态调试了一下,的确是一个巧合,未注释和注释的时候,指针的地址虽说有 8 个字节的偏差,但是正式因为这 8 个字节的偏差,导致指针指向的地址完全不一样,未注释的时候指针指向的地址指向了 code segment,注释后,指针指向的地址是合法的。
|
31
mrzys OP @tomychen 额,感觉没关系啊。我刚好写完了 rio,准备写代码测试的时候发现了这个蛋疼的问题。不过还好,解决了这个问题加深了对汇编和运行时堆栈的了解。
|
32
mrzys OP @pkookp8
环境:4.13.0-37-generic gcc version 5.4.0 20160609 ``` 两次未优化的汇编代码: ``` Dump of assembler code for function main: 0x0000000000400546 <+0>: push %rbp 0x0000000000400547 <+1>: mov %rsp,%rbp 0x000000000040054a <+4>: sub $0x10,%rsp 0x000000000040054e <+8>: mov -0x10(%rbp),%rax 0x0000000000400552 <+12>: mov %rax,%rsi 0x0000000000400555 <+15>: mov $0x400604,%edi 0x000000000040055a <+20>: mov $0x0,%eax 0x000000000040055f <+25>: callq 0x400430 <__isoc99_scanf@plt> 0x0000000000400564 <+30>: movq $0x0,-0x8(%rbp) 0x000000000040056c <+38>: mov $0x0,%eax 0x0000000000400571 <+43>: leaveq 0x0000000000400572 <+44>: retq ``` ``` Dump of assembler code for function main: 0x0000000000400546 <+0>: push %rbp 0x0000000000400547 <+1>: mov %rsp,%rbp 0x000000000040054a <+4>: sub $0x20,%rsp 0x000000000040054e <+8>: mov -0x18(%rbp),%rax 0x0000000000400552 <+12>: mov %rax,%rsi 0x0000000000400555 <+15>: mov $0x400604,%edi 0x000000000040055a <+20>: mov $0x0,%eax 0x000000000040055f <+25>: callq 0x400430 <__isoc99_scanf@plt> 0x0000000000400564 <+30>: movq $0x0,-0x10(%rbp) 0x000000000040056c <+38>: movq $0x0,-0x8(%rbp) 0x0000000000400574 <+46>: mov $0x0,%eax 0x0000000000400579 <+51>: leaveq 0x000000000040057a <+52>: retq End of assembler dump. ``` -0x18(%rbp)和 -0x10(%rbp)的值,一个是非法的一个是合法的 |