I have written a sample C code as below
#include<stdio.h>
int foo(void);
int main()
{
puts("Hello World");
int x = 10;
fprintf(stdout,"Hello World 22");
return foo();
}
I compile it via the
gcc -shared -fpic hello_world.c -o hello_world.so
above gcc command.
The assembly for the plt call for puts looks like as below
0000000000001070 <puts@plt>:
1070: f3 0f 1e fa endbr64
1074: f2 ff 25 9d 2f 00 00 bnd jmpq *0x2f9d(%rip) # 4018 <puts@GLIBC_2.2.5>
107b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Similarly for foo()
0000000000001080 <foo@plt>:
1080: f3 0f 1e fa endbr64
1084: f2 ff 25 95 2f 00 00 bnd jmpq *0x2f95(%rip) # 4020 <foo>
108b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
but the assembly looks the same.
I thought, the instructions are usually like this
But here it seems like it directly jumps to GLIBC. Have I understood something wrong ? or am i compiling it incorrectly ?
This has to do with -fcf-protection
. Ubuntu's GCC is configured with --enable-cet
, which enables -fcf-protection
instrumentation by default. If you compile with -fcf-protection=none
you will see the "normal" PLT entries that do JMP [<got-entry>]; PUSH <idx>; JMP <pltstub>
.
With -fcf-protection
(also =branch
or =full
), an additional section is added called .plt.sec
, where the actual function call stubs sit. Each stub starts with an ENDBR64
(see What does the endbr64 instruction actually do?) to mark it as a valid branching target and all JMP
instructions become BND JMP
(see Meaning of BND RET in x86). The PUSH <idx>
instructions to push the relocation index are all in .plt
, which now holds a sequence of ENDBR64; PUSH <idx>; BND JUMP <pltstub>
(as you can see below).
In the "normal" case, each GOT entry is initially filled with the address of the next instruction of the corresponding PLT entry.
In the case of .plt.sec
, the GOT entries are instead initially filled with .plt + <offset>
where <offset>
points to the corresponding sequence of ENDBR64; PUSH <idx>; BND JMP <pltstub>
in .plt
.
As to why this was designed like this, I am not sure. I found this perf
patch that seems to give some insights about .plt.sec
.
Here's how the PLT looks with -fcf-protection=none
, which is what you are expecting:
Disassembly of section .plt:
0000000000001020 <puts@plt-0x10>:
1020: ff 35 e2 2f 00 00 push 0x2fe2(%rip) # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: ff 25 e4 2f 00 00 jmp *0x2fe4(%rip) # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
102c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000001030 <puts@plt>:
1030: ff 25 e2 2f 00 00 jmp *0x2fe2(%rip) # 4018 <puts@GLIBC_2.2.5>
1036: 68 00 00 00 00 push $0x0
103b: e9 e0 ff ff ff jmp 1020 <_init+0x20>
And here's how it looks with -fcf-protection
:
Disassembly of section .plt:
0000000000001020 <.plt>:
1020: ff 35 e2 2f 00 00 push 0x2fe2(%rip) # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: f2 ff 25 e3 2f 00 00 bnd jmp *0x2fe3(%rip) # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
102d: 0f 1f 00 nopl (%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 push $0x0
1039: f2 e9 e1 ff ff ff bnd jmp 1020 <_init+0x20>
103f: 90 nop
1040: f3 0f 1e fa endbr64
1044: 68 01 00 00 00 push $0x1
1049: f2 e9 d1 ff ff ff bnd jmp 1020 <_init+0x20>
104f: 90 nop
1050: f3 0f 1e fa endbr64
1054: 68 02 00 00 00 push $0x2
1059: f2 e9 c1 ff ff ff bnd jmp 1020 <_init+0x20>
105f: 90 nop
Disassembly of section .plt.got:
0000000000001060 <__cxa_finalize@plt>:
1060: f3 0f 1e fa endbr64
1064: f2 ff 25 8d 2f 00 00 bnd jmp *0x2f8d(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
106b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .plt.sec:
0000000000001070 <puts@plt>:
1070: f3 0f 1e fa endbr64
1074: f2 ff 25 9d 2f 00 00 bnd jmp *0x2f9d(%rip) # 4018 <puts@GLIBC_2.2.5>
107b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)