各位大佬,求帮分析一下这段 C 代码

2018-03-22 12:50:38 +08:00
 mrzys

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

但是把下面给ij赋值的语句注释掉,或者只注释其中一条,却不会报错了:

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)

发现scanfch指针的地址赋值为0x0,这里的确是有问题,但是我想不通scanf下面的语句为什么会影响到程序的执行。

求各位大佬指教啊。。。

3648 次点击
所在节点    程序员
32 条回复
mengyaoss77
2018-03-22 15:50:31 +08:00
说是小白,玩的东西完全不像小白啊,直接反汇编。
反汇编看不懂,我只知道这个代码是危险的。就像 gets()一样
gunavy
2018-03-22 16:08:05 +08:00
就像 @pkookp8 做的实验一样,平台编译器都会有影响,对堆栈的划分和组织也不一样。要知道哪里的问题,只能汇编调试。
neoblackcap
2018-03-22 16:11:51 +08:00
为什么要研究 undefined behavior?这个发生什么事系统都是不作保证的
pkookp8
2018-03-22 16:16:05 +08:00
@pkookp8 试了 i386 的 gcc4.4.6
必定段错误。。。无法复现你说的屏蔽或不屏蔽现象不同
能否把出问题和不出问题的汇编都贴上来?
doun
2018-03-22 16:58:48 +08:00
@mrzys 下 8 个字节就是分配两个 int 啊兄弟
dummytaurus
2018-03-22 17:35:40 +08:00
这个只是巧合。进入 main 的时候,栈上第一个变量值是 nil,第二个指向栈上,第三个指向错误地址。所以 scanf 处理第三个变量会 segfault,而处理第一个(scanf 应该是有对 nil 做特殊处理)和第二个变量值没有问题。
注释前,gcc 无法优化,所以 ch 是第三个变量,指向错误地址。注释后,gcc 将优化掉 i 或者 j,不管怎样,ch 会变成第一个或者第二个变量,它的值都不会引起 scanf 崩溃。
不过栈上的初始值肯定是和 crt 相关的,macOS 上面两个示例都会蹦
tomychen
2018-03-22 17:43:19 +08:00
@mrzys #11 提到的

你早点说你在刷题和题型,就不会引那么多人猜了...
mrzys
2018-03-24 19:40:20 +08:00
@dummytaurus 我使用 gdb 打印了一下未初始化的指针的值,两次指针指向的地址不一样,没注释 i 和 j 的时候指针地址是合法的,注释其中一个的时候指针地址指向了 read-only code segment。的确和 crt 有关系。我怀疑是调用 main 之前已经使用了栈空间,栈上的值被上次的函数调用写入了数据,当调用 main 的时候,因为指针没有初始化,用的上次的值。
mrzys
2018-03-24 19:43:10 +08:00
@pkookp8 我自己测试环境是 ubuntu x86-64,用 macOS 也测试了一下,-O0 的时候会报错,但是-O1 的时候不会报错。
mrzys
2018-03-24 19:48:20 +08:00
@lingdux 大佬,我动态调试了一下,的确是一个巧合,未注释和注释的时候,指针的地址虽说有 8 个字节的偏差,但是正式因为这 8 个字节的偏差,导致指针指向的地址完全不一样,未注释的时候指针指向的地址指向了 code segment,注释后,指针指向的地址是合法的。
mrzys
2018-03-24 19:55:41 +08:00
@tomychen 额,感觉没关系啊。我刚好写完了 rio,准备写代码测试的时候发现了这个蛋疼的问题。不过还好,解决了这个问题加深了对汇编和运行时堆栈的了解。
mrzys
2018-03-24 20:01:49 +08:00
@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)的值,一个是非法的一个是合法的

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

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

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

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

© 2021 V2EX