V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mrzys
V2EX  ›  程序员

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

  •  
  •   mrzys · 2018-03-22 12:50:38 +08:00 · 3416 次点击
    这是一个创建于 2198 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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下面的语句为什么会影响到程序的执行。

    求各位大佬指教啊。。。

    32 条回复    2018-03-24 20:01:49 +08:00
    lance6716276
        1
    lance6716276  
       2018-03-22 12:55:19 +08:00 via Android
    先把代码写标准了再去 debug
    shuax
        2
    shuax  
       2018-03-22 12:57:03 +08:00
    char *ch 只是一个指针,都没有给他固定空间,然后就越界了。
    yinanc
        3
    yinanc  
       2018-03-22 13:01:12 +08:00
    赋值给未分配空间的指针时情况就不可控了,后面发生什么都有可能
    shsdust
        4
    shsdust  
       2018-03-22 13:06:59 +08:00
    不是指明了 Segmentation fault 吗?表示 CPU 试图访问无法定址的块,明显是因为指针没被分配空间,既然到这里已经出现了这种错误,下面的程序报错很正常的
    shsdust
        5
    shsdust  
       2018-03-22 13:07:39 +08:00
    还有,编译是一门玄学
    mrzys
        6
    mrzys  
    OP
       2018-03-22 13:16:19 +08:00
    @shsdust 但是这样并不会报错:
    ```
    #include <stdio.h>

    int main() {
    long i;
    long j;
    char *ch;
    scanf("%s", ch);
    i = 0;
    //j = 0;
    }
    ~
    ```
    这就是令人奇怪的地方。` scanf`后面的代码会影响到程序会不会发生异常退出。
    jiutai21
        7
    jiutai21  
       2018-03-22 13:19:27 +08:00
    未定义的行为把,换个平台 /编译器结果不一定的
    tomychen
        8
    tomychen  
       2018-03-22 13:20:09 +08:00
    1.ch 没空间
    2.注释掉那行后没出错有没有可能是因为刚好 scanf 到 ch 的值刚好复盖到了第二个 long 上,所以没报错,你可以试动态调试一下。或者简单的把输入内容调整>sizeof(long)。
    3. @shsdust 楼上说了玄学,这种东西代码在你的环境不出错不表示任何环境不出错,c 的指针野起来不是人能预见的。
    liuzhedash
        9
    liuzhedash  
       2018-03-22 14:14:05 +08:00
    @mrzys #6
    兄弟,这样写是否报错并不是你想的那种因果关系,建议阅读《深入理解计算机系统》的第三章。
    doun
        10
    doun  
       2018-03-22 14:17:14 +08:00 via Android
    大概因为 scanf 下面的 int i,j 编译后,也是挪到栈上的吧
    mrzys
        11
    mrzys  
    OP
       2018-03-22 14:47:40 +08:00
    @liuzhedash 就在阅读 csapp,但是对于内存这一块还是很模糊。第 7 章 linker 中说了局部变量是在执行的时候在 stack 上动态分配的,但是我反汇编之后,查看不到这段代码中的 i 和 j 的赋值指令。只有`sub $0x8,%rsp`,栈指针向下增加了 8 个字节,函数执行完毕返回之前执行`add $0x8,%rsp`,栈指针向上移动了 8 个字节,栈指针完全没有被影响。
    lingdux
        12
    lingdux  
       2018-03-22 14:52:36 +08:00
    那里的野指针需要申请空间,大小要足够容纳输入的内容。
    注释掉那句不发生异常只是堆栈压栈出栈的巧合而已,程序并没有按照你的预期执行。

    这种问题动态调式,注意一下堆栈和内存,一目了然。
    7 楼不懂别瞎说误导新人。
    mrzys
        13
    mrzys  
    OP
       2018-03-22 14:57:25 +08:00
    @lingdux 谢谢大佬,晚上回去动态调试一下。
    ysc3839
        14
    ysc3839  
       2018-03-22 15:01:40 +08:00 via Android
    @lingdux 想问一下 7 楼说的怎么误导了?
    lingdux
        15
    lingdux  
       2018-03-22 15:05:32 +08:00
    @ysc3839

    未定义的行为很扯淡
    lingdux
        16
    lingdux  
       2018-03-22 15:09:52 +08:00   ❤️ 1
    @mrzys
    这个问题让我想起来《 0day 安全:软件漏洞分析技术》里面一个例子
    虽然那个是 win 的,但是一样的原理。
    2 . 2 修改邻接变量中,修改了相邻的变量,填补了被破坏的堆栈,导致程序改变了流程实现了破解 CrackMe。
    pkookp8
        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
    不加其他编译参数
    pkookp8
        18
    pkookp8  
       2018-03-22 15:15:22 +08:00 via Android
    @pkookp8 尝试增加-g 和-O0 也没用。。。。
    ysc3839
        19
    ysc3839  
       2018-03-22 15:16:52 +08:00 via Android
    @lingdux 为什么这么说呢?
    lingdux
        20
    lingdux  
       2018-03-22 15:38:38 +08:00
    @ysc3839
    别问了,不想回复你了,感觉挺没意思的。
    mengyaoss77
        21
    mengyaoss77  
       2018-03-22 15:50:31 +08:00
    说是小白,玩的东西完全不像小白啊,直接反汇编。
    反汇编看不懂,我只知道这个代码是危险的。就像 gets()一样
    gunavy
        22
    gunavy  
       2018-03-22 16:08:05 +08:00
    就像 @pkookp8 做的实验一样,平台编译器都会有影响,对堆栈的划分和组织也不一样。要知道哪里的问题,只能汇编调试。
    neoblackcap
        23
    neoblackcap  
       2018-03-22 16:11:51 +08:00 via iPhone   ❤️ 1
    为什么要研究 undefined behavior?这个发生什么事系统都是不作保证的
    pkookp8
        24
    pkookp8  
       2018-03-22 16:16:05 +08:00 via Android
    @pkookp8 试了 i386 的 gcc4.4.6
    必定段错误。。。无法复现你说的屏蔽或不屏蔽现象不同
    能否把出问题和不出问题的汇编都贴上来?
    doun
        25
    doun  
       2018-03-22 16:58:48 +08:00 via Android
    @mrzys 下 8 个字节就是分配两个 int 啊兄弟
    dummytaurus
        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 上面两个示例都会蹦
    tomychen
        27
    tomychen  
       2018-03-22 17:43:19 +08:00
    @mrzys #11 提到的

    你早点说你在刷题和题型,就不会引那么多人猜了...
    mrzys
        28
    mrzys  
    OP
       2018-03-24 19:40:20 +08:00
    @dummytaurus 我使用 gdb 打印了一下未初始化的指针的值,两次指针指向的地址不一样,没注释 i 和 j 的时候指针地址是合法的,注释其中一个的时候指针地址指向了 read-only code segment。的确和 crt 有关系。我怀疑是调用 main 之前已经使用了栈空间,栈上的值被上次的函数调用写入了数据,当调用 main 的时候,因为指针没有初始化,用的上次的值。
    mrzys
        29
    mrzys  
    OP
       2018-03-24 19:43:10 +08:00
    @pkookp8 我自己测试环境是 ubuntu x86-64,用 macOS 也测试了一下,-O0 的时候会报错,但是-O1 的时候不会报错。
    mrzys
        30
    mrzys  
    OP
       2018-03-24 19:48:20 +08:00
    @lingdux 大佬,我动态调试了一下,的确是一个巧合,未注释和注释的时候,指针的地址虽说有 8 个字节的偏差,但是正式因为这 8 个字节的偏差,导致指针指向的地址完全不一样,未注释的时候指针指向的地址指向了 code segment,注释后,指针指向的地址是合法的。
    mrzys
        31
    mrzys  
    OP
       2018-03-24 19:55:41 +08:00
    @tomychen 额,感觉没关系啊。我刚好写完了 rio,准备写代码测试的时候发现了这个蛋疼的问题。不过还好,解决了这个问题加深了对汇编和运行时堆栈的了解。
    mrzys
        32
    mrzys  
    OP
       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)的值,一个是非法的一个是合法的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1578 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 16:58 · PVG 00:58 · LAX 09:58 · JFK 12:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.