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.