Function that returns address of local variable acts differently with different versions of gcc?

-1

I wrote this code and found that it acts differently with different versions of gcc.

The source code,

#include<stdio.h>

int *fun();

int main(int argc, char *argv[])
{
    int *ptr;

    ptr = fun();

    printf("%x", *ptr);
}

int *fun()
{
    int *ptr;
    int foo = 0xdeadbeef;
    ptr = &foo;

    return ptr;
}

The code is wrong. After execution of fun(), the local variable foo is released and doesn't exist. But the main function tries to use it, so it will lead segmentation fault.

But I tried the same code on three versions of gcc and they act differently.

In 10.2.0

╭─    ~ ································································ ✔ ─╮
╰─ gcc -v | bin/pbcopy                                                            ─╯
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: /build/gcc/src/gcc/configure --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.archlinux.org/ --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++,d --with-isl --with-linker-hash-style=gnu --with-system-zlib --enable-__cxa_atexit --enable-cet=auto --enable-checking=release --enable-clocale=gnu --enable-default-pie --enable-default-ssp --enable-gnu-indirect-function --enable-gnu-unique-object --enable-install-libiberty --enable-linker-build-id --enable-lto --enable-multilib --enable-plugin --enable-shared --enable-threads=posix --disable-libssp --disable-libstdcxx-pch --disable-libunwind-exceptions --disable-werror gdc_include_dir=/usr/include/dlang/gdc
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 10.2.0 (GCC)


╭─    ~ ································································ ✔ ─╮
╰─ gcc a.c && a.out                                                               ─╯
deadbeef%                                                                            

It prints deadbeef.

Its assembly code:

(gdb) disassemble fun 
Dump of assembler code for function fun:
   0x000000000000119d <+23>:    movl   $0xdeadbeef,-0x14(%rbp)
   0x00000000000011a4 <+30>:    lea    -0x14(%rbp),%rax
   0x00000000000011a8 <+34>:    mov    %rax,-0x10(%rbp)
   0x00000000000011ac <+38>:    mov    -0x10(%rbp),%rax
   0x00000000000011b0 <+42>:    mov    -0x8(%rbp),%rdx
   0x00000000000011b4 <+46>:    sub    %fs:0x28,%rdx
   0x00000000000011bd <+55>:    je     0x11c4 <fun+62>
   0x00000000000011bf <+57>:    call   0x1030 <__stack_chk_fail@plt>
   0x00000000000011c4 <+62>:    leave  
   0x00000000000011c5 <+63>:    ret    
End of assembler dump.

(gdb) disass main

   0x000000000000116c <+35>:    mov    %eax,%esi
   0x000000000000116e <+37>:    lea    0xe8f(%rip),%rdi        # 0x2004
   0x0000000000001175 <+44>:    mov    $0x0,%eax
   0x000000000000117a <+49>:    call   0x1040 <printf@plt>

Assembly code shows the function stores 0xdeadbeef in %rax, and printf receives it as %esi, so it prints 0xdeadbeef.

In 9.3.0:

coolder@ASUS:~$ gcc -v                                                          [1/1]
Using built-in specs.                                                                
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 9.3.0-15' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-0xEOmg/gcc-9-9.3.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-mutexThread model: posix
gcc version 9.3.0 (Debian 9.3.0-15)


coolder@ASUS:~$ gcc a.c && ./a.out
a.c: In function ‘fun’:
a.c:14:9: warning: function returns address of local variable [-Wreturn-local-addr]
   14 |  return &a;
      |         ^~
0coolder@ASUS:~$ 

It prints 0.

Its assembly code,

(gdb) disassemble fun
Dump of assembler code for function fun:
   0x000055555555515e <+0>:     push   %rbp
   0x000055555555515f <+1>:     mov    %rsp,%rbp
   0x0000555555555162 <+4>:     movl   $0xdeadbeef,-0x4(%rbp)
   0x0000555555555169 <+11>:    mov    $0x0,%eax
   0x000055555555516e <+16>:    pop    %rbp
(gdb) disass main
   0x000055555555513e <+9>:     call   0x55555555515e <fun>
   0x0000555555555143 <+14>:    mov    %rax,%rsi
   0x0000555555555146 <+17>:    lea    0xeb7(%rip),%rdi        # 0x555555556004
   0x000055555555514d <+24>:    mov    $0x0,%eax

Assembly code shows it moves 0 to %eax, and printf uses %eax as %rsi, so it prints 0.

In 5.4.1

➜  ~ gcc a.c && ./a.out 
a.c: In function ‘fun’:
a.c:17:9: warning: function returns address of local variable [-Wreturn-local-addr]
  return &a;
         ^
[1]    3566 segmentation fault (core dumped)  ./a.out

It gets segmentation fault, as I expected.

Its assembly code,

(gdb) disassemble fun 
Dump of assembler code for function fun:
   0x08048448 <+0>:     push   %ebp
   0x08048449 <+1>:     mov    %esp,%ebp
   0x0804844b <+3>:     sub    $0x10,%esp
   0x0804844e <+6>:     movl   $0xdeadbeef,-0x4(%ebp)
   0x08048455 <+13>:    mov    $0x0,%eax
   0x0804845a <+18>:    leave  
   0x0804845b <+19>:    ret   

(gdb) disass main
   0x0804841d <+17>:    call   0x8048448 <fun>
   0x08048422 <+22>:    mov    %eax,-0xc(%ebp)
   0x08048425 <+25>:    mov    -0xc(%ebp),%eax
   0x08048428 <+28>:    mov    (%eax),%eax

Assembly code shows that it moves 0x0 to %eax, and main tries to refer %eax, so this leads to segmentation fault.

So why the assembly code is so different?

Any help will be appreciated.

c
assembly
gcc
segmentation-fault
undefined-behavior
asked on Stack Overflow Mar 13, 2021 by coolder • edited Mar 14, 2021 by Peter Cordes

1 Answer

4

Returning the address of a local variable and trying to access it after its lifetime is over is undefined behavior, rationalizing what happens under the hood is a fool's errand because there are no standard rules to be followed (appart, of course, from the aforementioned and linked UB rules), it's quite common different compiler versions changing the way a situation like this is dealt with.

answered on Stack Overflow Mar 13, 2021 by anastaciu • edited Mar 13, 2021 by anastaciu

User contributions licensed under CC BY-SA 3.0