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

talk is cheap,来观摩一下简单代码

  •  
  •   microxiaoxiao · 2022-05-27 09:35:02 +08:00 · 4990 次点击
    这是一个创建于 912 天前的主题,其中的信息可能已经有所发展或是发生改变。
    void test(char *p, int len)
    {
    snprintf(out,len,"%s","hello world");
    }
    int main()
    {
    char res[512];
    {
    char result[1024];
    test(result,1024);
    }
    printf("result %s\n",res);
    }

    问题:在 ubuntu20.04 上面会输出 hello world 。
    其他环境;公司自研系统正常,suse12 正常
    问题,为啥 ubuntu 有这种骚操作->把 res 和 result 搞成同一个地址。
    手机打字不容易,不好排版,见谅。
    32 条回复    2022-05-28 16:12:39 +08:00
    eote
        1
    eote  
       2022-05-27 09:40:33 +08:00
    c 里面数组初始化都不分配默认值的吗?那里面有啥都不奇怪
    MoYi123
        2
    MoYi123  
       2022-05-27 09:46:11 +08:00
    有没有可能你需要 memset 一下 res.
    microxiaoxiao
        3
    microxiaoxiao  
    OP
       2022-05-27 09:50:04 +08:00
    @eote 不是说他默认值的问题,是 res 地址和 result 地址一致,导致在调用的时候被覆盖了。在 test 中写入的就是 res 的地址。
    microxiaoxiao
        4
    microxiaoxiao  
    OP
       2022-05-27 09:52:54 +08:00
    @eote memset 和直接初始化当然应该,我的主要问题是它把地址优化为一个地址是不是不太对,因为如果,printf 之前对他进行付初值已经晚了
    wuruorocks
        5
    wuruorocks  
       2022-05-27 09:58:42 +08:00
    第 3 行的 out 在哪里定义的
    villivateur
        6
    villivateur  
       2022-05-27 09:59:21 +08:00
    在你的 test 函数里面,snprintf 的 len 参数 是 1024 ,而 "hello world" 又那么短,你确定不会发生内存访问错误?
    shyrock
        7
    shyrock  
       2022-05-27 09:59:32 +08:00
    snprintf(out,len,"%s","hello world");

    这句里面的 out 是啥?
    microxiaoxiao
        8
    microxiaoxiao  
    OP
       2022-05-27 10:01:27 +08:00
    @wuruorocks 写错了,就是 result 手机打字麻烦,函数那个 p 写成 out
    microxiaoxiao
        9
    microxiaoxiao  
    OP
       2022-05-27 10:02:07 +08:00
    @shyrock 手机打字,out 写成 p
    microxiaoxiao
        10
    microxiaoxiao  
    OP
       2022-05-27 10:03:08 +08:00
    void test(char *p, int len)
    {
    snprintf(p,len,"%s","hello world");
    }修改一下
    eote
        11
    eote  
       2022-05-27 10:06:20 +08:00   ❤️ 2
    @microxiaoxiao

    gcc 根本没申请 res[512]


    --前略

    main:
    .LFB1:
    .cfi_startproc
    endbr64
    pushq %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq %rsp, %rbp
    .cfi_def_cfa_register 6
    subq $1040, %rsp
    movq %fs:40, %rax
    movq %rax, -8(%rbp)
    xorl %eax, %eax
    leaq -1040(%rbp), %rax
    movl $1024, %esi
    movq %rax, %rdi
    call test
    leaq -1040(%rbp), %rax
    movq %rax, %rsi
    leaq .LC2(%rip), %rdi
    movl $0, %eax
    call printf@PLT
    movl $0, %eax
    movq -8(%rbp), %rdx
    xorq %fs:40, %rdx
    je .L4
    call __stack_chk_fail@PLT
    -- 后略
    chenxytw
        12
    chenxytw  
       2022-05-27 10:06:58 +08:00   ❤️ 2
    和操作系统无关,和编译器有关系。
    Ubuntu 20.04 默认是 gcc9, suse12 默认是 gcc7 。
    未初始化的变量是 UB 行为,编译器理论上怎么做都没关系。
    变量声明是没有先后顺序的。
    ksco
        13
    ksco  
       2022-05-27 10:08:46 +08:00
    初始化一下 res:char res[512] = {0};
    senninha
        14
    senninha  
       2022-05-27 10:09:23 +08:00
    编译器版本不一样导致的,看一下汇编代码吧
    另外,在不会输出 hello world 的版本上 -O3 优化一下,估计也是 hello world.
    lonestar
        15
    lonestar  
       2022-05-27 10:16:09 +08:00
    有意思。ubuntu 18/gcc 7.5 也是这样。即使 -O0 也还是这样。

    考虑到在进入下级 { } 前这些局部变量并未使用,那么这样的行为也没有危害。只要在进入内部 { } 前随便引用一下 res ,编译器就会为 res 真正分配空间了。
    DianQK
        16
    DianQK  
       2022-05-27 10:27:08 +08:00
    试了一下 clang 在 ubuntu 、macos 、arch 上都是正常的行为,而 gcc 在 -O0 -O1 下都会出错,-O2 -O3 是没问题的。
    gcc 为这两个变量分配同一个地址没什么问题,但是看起来 -O0 下因为什么原因没有释放这部分内存(难不成是 bug )
    DianQK
        17
    DianQK  
       2022-05-27 10:37:46 +08:00
    说错了,栈上的内存哪有什么释放的一说,如上所说,**当使用一个 char[] 时候应当进行初始化。如果没初始化就会进入未定义行为。**
    事实上由于未定义行为,clang 一样会坏掉。
    encro
        18
    encro  
       2022-05-27 10:42:28 +08:00
    你适合 rust ,哈哈
    luassuns
        19
    luassuns  
       2022-05-27 10:43:45 +08:00
    suse gcc 12.1/clang14 没有这个问题
    DianQK
        20
    DianQK  
       2022-05-27 10:44:35 +08:00
    有一个方式理解起来可能容易一些,把 res 和 result 当成一个普通的 int 变量。
    由于在 { result } 的逻辑前面没有 res 的 define 和 use ,同时 { result } 完成后,result 不是 live (后面没有逻辑在使用 result )的,为了优化性能 result 自然可以复用 res 的地址 /寄存器。
    但是 result 的内容在栈 /寄存器上没有释放(我是指把 result 用到的内存 /寄存器设置为默认值)。

    (感觉自己表述的很烂,不知道我的思考是不是对的,应该差不多了
    关键还是使用一个变量但没有初始化,那这个变量就可能指向一个脏的空间(别人用过的)。
    ksco
        21
    ksco  
       2022-05-27 10:45:24 +08:00   ❤️ 1
    @DianQK #17 不写 UB 的代码就好了,纠结 UB 属于费力不讨好。
    mingl0280
        22
    mingl0280  
       2022-05-27 10:58:40 +08:00
    使用未初始化的变量是未定义行为,无讨论价值。
    建议锁帖。
    codehz
        23
    codehz  
       2022-05-27 10:59:08 +08:00
    @DianQK 局部变量哪有什么“释放”的概念。。。
    DianQK
        24
    DianQK  
       2022-05-27 11:17:38 +08:00
    @codehz 我表述错了
    sudoy
        25
    sudoy  
       2022-05-27 11:20:27 +08:00
    没排版的代码看着好难受
    microxiaoxiao
        26
    microxiaoxiao  
    OP
       2022-05-27 11:24:06 +08:00 via Android
    大家都很有见地,我就不一一回复了,打字不方便。感谢大家。这个问题综合大家说的,我的初步结论是这样的:char res[],过程其实并不分配内存,只有在真正引用的过程才会分配内存,咋们平时说的初始化和 memset print 都是针对它的引用。行为其实是定义了的,有些编译器可能给它分配到的是为使用过的地址,有些分配到的是未使用过的地址,表现为未定义,这样就会有所谓的数据随机化。感谢给汇编的思路。
    smdbh
        27
    smdbh  
       2022-05-27 11:42:15 +08:00
    作用域问题把
    weiwenhao
        28
    weiwenhao  
       2022-05-27 12:02:06 +08:00
    测试了一下,确实是 26 楼主这样的结论。

    发现这里大佬这么多,刚好也有一个疑惑点,在所有变量都初始化的情况下
    ```
    int8_t a = 1
    {
    int8_t b = 2
    }
    int8_t c = 3
    ```

    类似这种情况,int8_t b 的作用域离开后不久没用了吗, int8_t b 和 int8_t c 完全可以使用同一段栈空间, 这样不是更节省空间吗, 但是实际上 gcc 编译器生成的代码是这样的, 为每个变量都分配了栈空间,完全没有考虑作用域? 这样做有啥好处吗
    movb $1, -3(%rbp)
    movb $2, -2(%rbp)
    movb $3, -1(%rbp)
    iamzuoxinyu
        29
    iamzuoxinyu  
       2022-05-27 13:01:23 +08:00
    @weiwenhao C 没有 RAII 。只有作用域访问限制。
    Caturra
        30
    Caturra  
       2022-05-27 13:51:49 +08:00
    虽然说不是一定要全部初始化,但你不初始化那么`res`的内容完全可以任由编译器去解释
    我丢到 godbolt 上看确实不同的 gcc 版本结果不一样,生成的指令各不相同就不细看了

    其中一种可以解释为:
    1. 因为你用`result`前(调用`test`)的代码并没有用到`res`
    2. gcc 打算把它放到`result`的前 512 字节中,既在`printf`前其实只有栈指针 rsp - 1024 而不是 1024+512
    3. 而`res`从起始地址开始就因为`test`被塞入了 hello world ,所以也会莫名其妙输出 hello world

    其实意思就是编译器把原来第 2 行的`res`定义优化了,放到原来第 4 行后面,栈指针可以省点偏移量

    要避免这种 UB ,又舍不得`memset`整个`res`的话,可以用`res[0] = 0`,直接在首部塞入结束符
    不过写这种代码我觉得还是别像资本家发工资那么抠门吧,直接 memset 就好了
    DianQK
        31
    DianQK  
       2022-05-27 14:06:12 +08:00
    @weiwenhao O0 编译?改成 O1 这三个变量就会用同一个寄存器了
    (不过我不知道怎么避免执行 DCE ,我加了个辅助函数

    extern void use_i(int8_t);

    int main() {
    int8_t a = 1;
    use_i(a);
    {
    int8_t b = 2;
    use_i(b);
    }
    int8_t c = 3;
    use_i(c);
    }
    secondwtq
        32
    secondwtq  
       2022-05-28 16:12:39 +08:00
    @weiwenhao 这问题没法回答,因为编译器优化是个 best effort 的过程,你不能保证他一定会给你优化
    不优化不一定是“有什么好处”,有可能只是触发了一个 bug 或者 edge case ,这是和具体实现相关的
    现在的大多数 C/C++ 程序员被灌输的认知一般是“编译器很聪明”(甚至 JavaScript 等语言的程序员也出现了这种现象),但实际上编译器有时候也很笨。客观的来说,编译器优化只是一个暴力循环自动算法的过程而已。有时候灵,有时候不灵。

    但是只要程序的可见行为符合标准,原则上就没问题
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1004 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 19:10 · PVG 03:10 · LAX 11:10 · JFK 14:10
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.