0

I have the following program to test the outcome of uninitialized pointers by different compilers

#include<stdio.h>
#include<stdlib.h>

typedef struct intNode
{
    int intval;
    struct intNode* link;
}* TOKEN;

TOKEN talloc()
{
    TOKEN tok = (TOKEN)malloc(sizeof(struct intNode));
    tok->intval = 0;
    tok->link = NULL;
    return tok;
}


TOKEN makefloat(TOKEN tok)
{
    TOKEN result;
    if (tok->intval > 10)
        result = tok;
    else
    {
        TOKEN result = talloc();
        result->intval = -10;
    }
    return result;
}

void printexpr(TOKEN tok)
{
    printf("tok: %p, tok->link: %p\n", tok, tok->link);
    if (tok != NULL)
    {
        printf("%d ", tok->intval);
        while(tok->link != NULL)
        {
            tok = tok->link;
            printf("-> %d ", tok->intval);
        }
        printf("\n");
    }
    else
        printf("Tok is NULL\n");
}


int main(int argc, char *argv[])
{
    TOKEN rhs, lhs;
    rhs = talloc();
    lhs = talloc();
    printf("BEFORE Get the address of rhs: %p, lhs: %p\n", rhs, lhs);
    lhs->intval = 1982;
    rhs->intval = 5;
    rhs = makefloat(rhs);
    printf("AFTER Get the address of rhs: %p, lhs: %p\n", rhs, lhs);
    lhs->link = rhs;
    printexpr(lhs);

    return 0;
}

And my test platform is a Ubuntu 18.04 desktop. I tried to compile the program with gcc-5, gcc-6, gcc-7, gcc-8, gcc-9, and clang-9 and I got different outputs as follows:

Result of gcc-5, gcc-6, gcc-7:

AFTER Get the address of rhs: 0x1, lhs: 0x5629d3959280
tok: 0x5629d3959280, tok->link: 0x1
1982 Segmentation fault (core dumped)

Every time I run the executable, the address of lhs (tok) is different, since it is malloc-ed. But the address of rhs (tok->link) is always 0x1. I find out the reason after some debugging, which is explained later.

Result of gcc-8 and gcc-9:

AFTER Get the address of rhs: 0xc2, lhs: 0x557f7dbdf280
tok: 0x557f7dbdf280, tok->link: 0xc2
1982 Segmentation fault (core dumped)

The difference from the previous result is that now the address of rhs(tok->link) is always 0xc2. The reason is explained later.

Result of clang-9 (with printf before calling printexpr in main):

AFTER Get the address of rhs: 0x7ffc10d6c418, lhs: 0x141d280
tok: 0x141d280, tok->link: 0x7ffc10d6c418
1982 -> 282510568 Segmentation fault (core dumped)

Every time I run the executable, the addresses of both rhs and lhs are different. And because the address of rhs is a valid address (instead of 0xc2 or 0x1 in the previous cases), its intval also gets printed.

However, if I remove the printf before calling printexpr in main, the result of clang-9 is as follows:

tok: 0x1a7c280, tok->link: 0xc2
1982 Segmentation fault (core dumped)

I got the same result as for gcc-8 and gcc-9. (I tried to add printf's inside printexpr but it makes no difference; I also tried to add fflush(stdout) after printf's but it still makes no difference.)

I find that gcc-x always gives consistent results whether there are printf's before calling printexpr or not.

My question 1: Why does the output of clang-9 depend on printf? Or why it seems to be.

My debugging findings: When I use gdb to debug the program, I find that the return value of makefloat is an uninitialized 8-byte data on the stack starting from $rbp-0x10. Here is the dump assembly of makefloat()

Dump of assembler code for function makefloat:
   0x00005555555547b7 <+0>:     push   %rbp
   0x00005555555547b8 <+1>:     mov    %rsp,%rbp
   0x00005555555547bb <+4>:     sub    $0x20,%rsp
   0x00005555555547bf <+8>:     mov    %rdi,-0x18(%rbp)
   0x00005555555547c3 <+12>:    mov    -0x18(%rbp),%rax
   0x00005555555547c7 <+16>:    mov    (%rax),%eax
   0x00005555555547c9 <+18>:    cmp    $0xa,%eax
   0x00005555555547cc <+21>:    jle    0x5555555547d8 <makefloat+33>
   0x00005555555547ce <+23>:    mov    -0x18(%rbp),%rax
   0x00005555555547d2 <+27>:    mov    %rax,-0x10(%rbp)
   0x00005555555547d6 <+31>:    jmp    0x5555555547f0 <makefloat+57>
   0x00005555555547d8 <+33>:    mov    $0x0,%eax
   0x00005555555547dd <+38>:    callq  0x555555554785 <talloc>
   0x00005555555547e2 <+43>:    mov    %rax,-0x8(%rbp)
   0x00005555555547e6 <+47>:    mov    -0x8(%rbp),%rax
   0x00005555555547ea <+51>:    movl   $0xfffffff6,(%rax)
   0x00005555555547f0 <+57>:    mov    -0x10(%rbp),%rax   <--- this is the return value of makefloat
   0x00005555555547f4 <+61>:    leaveq 
   0x00005555555547f5 <+62>:    retq   
End of assembler dump.

The address of the 8-byte data is always the same every time I run the program. Therefore, I try to see the value in this address at the beginning of _start() and I always get 0xc2. Note that this is true for all compilers.

So, gcc-8 and gcc-9 generated code does not overwrite this address. However, gcc-5,6,7 overwrite the address with 0x1 somewhere between _start() and makefloat(). After some more debugging, I find that the devil is in frame_dummy() function, which is called in __libc_csu_init(), which is called in __libc_start_main(). Here is the frame_dummy() function generated by gcc-8 and gcc-9:

Dump of assembler code for function frame_dummy:
=> 0x0000555555554780 <+0>: jmpq   0x555555554700 <register_tm_clones>
End of assembler dump.

which jumps directly to register_tm_clones without pushing anything on the stack. Here is the result generated by gcc-5,6,7:

Dump of assembler code for function frame_dummy:
   0x00005555555547a0 <+0>: push   %rbp
   0x00005555555547a1 <+1>: mov    %rsp,%rbp
   0x00005555555547a4 <+4>: pop    %rbp
   0x00005555555547a5 <+5>: jmpq   0x555555554710 <register_tm_clones>
End of assembler dump.

which pushes $rbp on the stack (and later pops out) before jumping to register_tm_clones. This happens to overwrite the 8-byte data on the stack, which will be used as the return value of makefloat. The pushed %rbp value in this case is 0x1.

This explains the 0x1 value in the output of gcc-5,6,7. But it still does not explain my second question:

My question 2: Why the initial value in the 8-byte stack memory always 0xc2? I have a very shallow understanding of the linking and loading process of x86-64 executables. But I would like to read more. It is highly appreciated if anyone can tell me where to start to investigate.

  • 5
    "I understand that in makefloat, the returned result is uninitialized if the control flow goes to the else part. " - then just fix that bug. You have undefined behavior if you use uninitalized values; anything can happen. – Mat Dec 03 '19 at 22:21
  • 1
    (Also: really not a fan of typedef'ing a pointer if it is not for an opaque type. https://stackoverflow.com/questions/750178/is-it-a-good-idea-to-typedef-pointers) – Mat Dec 03 '19 at 22:25
  • 4
    Undefined behavior is undefined. Compile to assembly and examine that to see what the code does, just keep in mind that different compilation might produce assembly that does different thing (I mean, undefined behavior means just that: the standard C does not specify what the code should do, at all). – hyde Dec 03 '19 at 22:30
  • The compiler does things that would work if your program did not contain UB , it cares nothing about what happens for UB programs – M.M Dec 03 '19 at 22:45

0 Answers0