I'm investigating the size of an extremely small C program on Linux (ubuntu 20.04).
I'm compiling as follows:
gcc -s -nostdlib test.c -o test
the following progam:
__attribute__((naked))
void _start() {
asm("movl $1,%eax;"
"xorl %ebx,%ebx;"
"int $0x80");
}
Basically the idea is to make the Linux system call to exit rather than depending on the C runtime to do that for us. (which would be the case in void main() { }). The program moves 1 into register EAX, clears register EBX (which would otherwise contain the return value), and then executes the linux system call interrupt 0x80. This interrupt triggers the kernel to process our call.
I would expect this program to be extremely small (less than 1K), however ..
du -h test
# >> 16K
ldd test
# >> statically linked
Why is this program still 16K?
du reports the disk space used by a file whereas ls reports the actual size of a file. Typically the size reported by du is significantly larger for small files.
You can significantly reduce the size of the binary by changing compile and linking options and stripping out unnecessary sections.
$ cat test.c
void _start() {
asm("movl $1,%eax;"
"xorl %ebx,%ebx;"
"int $0x80");
}
$ gcc -s -nostdlib test.c -o test
$ ./test
$ ls -l test
-rwxrwxr-x 1 fpm fpm 8840 Dec 9 04:09 test
$ readelf -W --section-headers test
There are 7 section headers, starting at offset 0x20c8:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000400190 000190 000024 00 A 0 0 4
[ 2] .text PROGBITS 0000000000401000 001000 000010 00 AX 0 0 1
[ 3] .eh_frame_hdr PROGBITS 0000000000402000 002000 000014 00 A 0 0 4
[ 4] .eh_frame PROGBITS 0000000000402018 002018 000038 00 A 0 0 8
[ 5] .comment PROGBITS 0000000000000000 002050 00002e 01 MS 0 0 1
[ 6] .shstrtab STRTAB 0000000000000000 00207e 000045 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
$
$ gcc -s -nostdlib -Wl,--nmagic test.c -o test
$ ls -l test
-rwxrwxr-x 1 fpm fpm 984 Dec 9 16:55 test
$ strip -R .comment -R .note.gnu.build-id test
$ strip -R .eh_frame_hdr -R .eh_frame test
$ ls -l test
-rwxrwxr-x 1 fpm fpm 520 Dec 9 17:03 test
$
Note that clang can produce a significantly smaller binary than gcc by default in this particular instance. However, after compiling with clang and stripping unnecessary sections, the final size of the binary is 736 bytes, which is bigger than the 520 bytes possible with gcc -s -nostdlib -Wl,--nmagic test.c -o test.
$ clang -static -nostdlib -flto -fuse-ld=lld -o test test.c
$ ls -l test
-rwxrwxr-x 1 fpm fpm 1344 Dec 9 04:15 test
$
$ readelf -W --section-headers test
There are 9 section headers, starting at offset 0x300:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000200190 000190 000018 00 A 0 0 4
[ 2] .eh_frame_hdr PROGBITS 00000000002001a8 0001a8 000014 00 A 0 0 4
[ 3] .eh_frame PROGBITS 00000000002001c0 0001c0 00003c 00 A 0 0 8
[ 4] .text PROGBITS 0000000000201200 000200 00000f 00 AX 0 0 16
[ 5] .comment PROGBITS 0000000000000000 00020f 000040 01 MS 0 0 1
[ 6] .symtab SYMTAB 0000000000000000 000250 000048 18 8 2 8
[ 7] .shstrtab STRTAB 0000000000000000 000298 000055 00 0 0 1
[ 8] .strtab STRTAB 0000000000000000 0002ed 000012 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
$
$ strip -R .eh_frame_hdr -R .eh_frame test
$ strip -R .comment -R .note.gnu.build-id test
strip: test: warning: empty loadable segment detected at vaddr=0x200000, is this intentional?
$ ls -l test
-rwxrwxr-x 1 fpm fpm 736 Dec 9 04:19 test
$ readelf -W --section-headers test
There are 3 section headers, starting at offset 0x220:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000000201200 000200 00000f 00 AX 0 0 16
[ 2] .shstrtab STRTAB 0000000000000000 00020f 000011 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
$
.text is your code, .shstrtab is the Section Header String table. Each ElfHeader structure contains an e_shstrndx member which is an index into the .shstrtab table. If you use this index, you can find the name of that section.
du, by default, reports the space used on disk by the file--this means that the smallest value will be one disk block. If you want to know the actual size of the file, use ls -l.
The modified program to get an exit code equal to 3 (for the fun):
void _start() {
asm("movl $1,%eax;"
"movl $3,%ebx;"
"int $0x80");
}
The build with:
-s
Remove all symbol table and relocation information from the executable.
-nostdlib
Do not use the standard system startup files or libraries when linking.
$ gcc -s -nostdlib pg.c -o pg
$ ./pg
$ echo $?
3
$ ldd ./pg
statically linked
The size of the resulting executable file is 13 KB:
$ ls -l ./pg
-rwxrwxr-x 1 xxx xxx 13296 dec. 9 11:42 ./pg
The code disassembly shows that the text section is actually 23 bytes long and there is no data:
$ objdump -S ./pg
./pg: file format elf64-x86-64
Disassembly of section .text:
0000000000001000 <.text>:
1000: f3 0f 1e fa endbr64
1004: 55 push %rbp
1005: 48 89 e5 mov %rsp,%rbp
1008: b8 01 00 00 00 mov $0x1,%eax
100d: bb 03 00 00 00 mov $0x3,%ebx
1012: cd 80 int $0x80
1014: 90 nop
1015: 5d pop %rbp
1016: c3 retq
But the size utility shows that the data section is 224 (size of the .dynamic section) and the size reported for the text is 248 bytes. This is the total size of the other sections minus the .comment:
$ size pg
text data bss dec hex filename
248 224 0 472 1d8 pg
$ size pg --format=SysV
pg :
section size addr
.interp 28 792
.note.gnu.property 32 824
.note.gnu.build-id 36 856
.gnu.hash 28 896
.dynsym 24 928
.dynstr 1 952
.text 23 4096
.eh_frame_hdr 20 8192
.eh_frame 56 8216
.dynamic 224 16160
.comment 42 0
Total 514
If we rebuild the program adding:
-static
On systems that support dynamic linking, this overrides -pie and prevents linking with the shared libraries. On other systems, this option has no effect.
The size of the file decreases (8816 bytes instead of 13 KB):
$ gcc -s -static -nostdlib pg.c -o pg
$ ls -l ./pg
-rwxrwxr-x 1 xxxx xxxx 8816 dec. 9 13:03 ./pg
The "-static" option made disappear several dynamic linking related sections:
$ size pg
text data bss dec hex filename
147 0 0 147 93 pg
$ size pg --format=SysV
pg :
section size addr
.note.gnu.property 32 4194760
.note.gnu.build-id 36 4194792
.text 23 4198400
.eh_frame 56 4202496
.comment 42 0
Total 189
Related
Any experts with a deep understanding of ELF loading, could you please explain to me why the following ELF file throws a Segmentation fault (errno=139)?
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x131a
Start of program headers: 64 (bytes into file)
Start of section headers: 232 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 3
Size of section headers: 64 (bytes)
Number of section headers: 8
Section header string table index: 7
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] null NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .init PROGBITS 000000000000131a 0000031a
0000000000000001 0000000000000000 AX 0 0 1
[ 2] .text PROGBITS 000000000000131b 0000031b
0000000000000096 0000000000000000 AX 0 0 1
[ 3] .fini PROGBITS 00000000000013b1 000003b1
0000000000000001 0000000000000000 AX 0 0 1
[ 4] .rodata PROGBITS 00000000000013b2 000003b2
0000000000000014 0000000000000000 A 0 0 1
[ 5] .data PROGBITS 00000000000013c6 000003c6
000000000000001e 0000000000000000 A 0 0 1
[ 6] .bss NOBITS 00000000000013e4 000003e4
0000000000000000 0000000000000000 WA 0 0 1
[ 7] strtab STRTAB 00000000000012e8 000002e8
0000000000000032 0000000000000000 AS 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x000000000000031a 0x000000000000131a 0x000000000000131a
0x0000000000000098 0x0000000000000098 R E 0x1000
LOAD 0x00000000000003b2 0x00000000000013b2 0x00000000000013b2
0x0000000000000014 0x0000000000000014 R 0x1000
LOAD 0x00000000000003c6 0x00000000000013c6 0x00000000000013c6
0x000000000000001e 0x000000000000101e RW 0x1000
Section to Segment mapping:
Segment Sections...
00 .init .text .fini
01 .rodata
02 .data .bss
The exact same executable with the following file alignment changes works fine:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x10400
Start of program headers: 64 (bytes into file)
Start of section headers: 232 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 3
Size of section headers: 64 (bytes)
Number of section headers: 8
Section header string table index: 7
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] null NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .init PROGBITS 0000000000010400 00000400
0000000000000001 0000000000000000 AX 0 0 1
[ 2] .text PROGBITS 0000000000010800 00000800
0000000000000096 0000000000000000 AX 0 0 1
[ 3] .fini PROGBITS 0000000000010c00 00000c00
0000000000000001 0000000000000000 AX 0 0 1
[ 4] .rodata PROGBITS 0000000000011000 00001000
0000000000000014 0000000000000000 A 0 0 1
[ 5] .data PROGBITS 0000000000011400 00001400
000000000000001e 0000000000000000 A 0 0 1
[ 6] .bss NOBITS 0000000000011800 00001800
0000000000000000 0000000000000000 WA 0 0 1
[ 7] strtab STRTAB 00000000000102e8 000002e8
0000000000000032 0000000000000000 AS 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000400 0x0000000000010400 0x0000000000010400
0x0000000000000801 0x0000000000000801 R E 0x1
LOAD 0x0000000000001000 0x0000000000011000 0x0000000000011000
0x0000000000000014 0x0000000000000014 R 0x1
LOAD 0x0000000000001400 0x0000000000011400 0x0000000000011400
0x000000000000001e 0x000000000000101e RW 0x1
Section to Segment mapping:
Segment Sections...
00 .init .text .fini
01 .rodata
02 .data .bss
In both cases it holds that:
sh_addr mod sh_addralign = 0 and
p_vaddr mod PAGESIZE = p_offset. (Pagesize acquired with getconf PAGESIZE).
I appreciate your help - thank you very much in advance.
UPDATE:
I realized that my LOAD segments were overlapping in virtual memory in the first readelf printout that I posted. I have corrected this now, but for the now non-overlapping LOAD segments I still get a segmentation fault when my start virtual memory address for the first page is at 0x0 (same if it is at 0x1000, i.e. one page size higher):
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x31a
Start of program headers: 64 (bytes into file)
Start of section headers: 232 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 3
Size of section headers: 64 (bytes)
Number of section headers: 8
Section header string table index: 7
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] null NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .init PROGBITS 000000000000031a 0000031a
0000000000000001 0000000000000000 AX 0 0 0
[ 2] .text PROGBITS 000000000000031b 0000031b
0000000000000076 0000000000000000 AX 0 0 0
[ 3] .fini PROGBITS 0000000000000391 00000391
0000000000000001 0000000000000000 AX 0 0 0
[ 4] .rodata PROGBITS 0000000000001392 00000392
0000000000000014 0000000000000000 A 0 0 0
[ 5] .data PROGBITS 00000000000023a6 000003a6
000000000000001e 0000000000000000 A 0 0 0
[ 6] .bss NOBITS 00000000000023c4 000003c4
0000000000000000 0000000000000000 WA 0 0 0
[ 7] strtab STRTAB 00000000000002e8 000002e8
0000000000000032 0000000000000000 AS 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x000000000000031a 0x000000000000031a 0x000000000000031a
0x0000000000000078 0x0000000000000078 R E 0x1000
LOAD 0x0000000000000392 0x0000000000001392 0x0000000000001392
0x0000000000000014 0x0000000000000014 R 0x1000
LOAD 0x00000000000003a6 0x00000000000023a6 0x00000000000023a6
0x000000000000001e 0x0000000000000082 RW 0x1000
Section to Segment mapping:
Segment Sections...
00 .init .text .fini
01 .rodata
02 .data .bss
When I change the start address to 0x10000 (PAGESIZE * 16), then the segmentation fault disappears. Any ideas why that is?
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1031a
Start of program headers: 64 (bytes into file)
Start of section headers: 232 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 3
Size of section headers: 64 (bytes)
Number of section headers: 8
Section header string table index: 7
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] null NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .init PROGBITS 000000000001031a 0000031a
0000000000000001 0000000000000000 AX 0 0 0
[ 2] .text PROGBITS 000000000001031b 0000031b
0000000000000076 0000000000000000 AX 0 0 0
[ 3] .fini PROGBITS 0000000000010391 00000391
0000000000000001 0000000000000000 AX 0 0 0
[ 4] .rodata PROGBITS 0000000000011392 00000392
0000000000000014 0000000000000000 A 0 0 0
[ 5] .data PROGBITS 00000000000123a6 000003a6
000000000000001e 0000000000000000 A 0 0 0
[ 6] .bss NOBITS 00000000000123c4 000003c4
0000000000000000 0000000000000000 WA 0 0 0
[ 7] strtab STRTAB 00000000000002e8 000002e8
0000000000000032 0000000000000000 AS 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x000000000000031a 0x000000000001031a 0x000000000001031a
0x0000000000000078 0x0000000000000078 R E 0x1000
LOAD 0x0000000000000392 0x0000000000011392 0x0000000000011392
0x0000000000000014 0x0000000000000014 R 0x1000
LOAD 0x00000000000003a6 0x00000000000123a6 0x00000000000123a6
0x000000000000001e 0x0000000000000082 RW 0x1000
Section to Segment mapping:
Segment Sections...
00 .init .text .fini
01 .rodata
02 .data .bss
UPDATE 2:
Thank you Employed Russian for your answer and ideas. I wanted to share the following update on my own research:
After digging a bit more, I ran across the following line in an Oracle document about program loading:
By default, 64–bit SPARC programs are linked with a starting address of 0x100000000. The whole program is located above 4 gigabytes, including its text, data, heap, stack, and shared object dependencies. This helps ensure that 64–bit programs are correct because the program will fault in the least significant 4 gigabytes of its address space if the program truncates any of its pointers. While 64–bit programs are linked above 4 gigabytes, you can still link programs below 4 gigabytes by using a mapfile and the -M option to the link-editor. See /usr/lib/ld/sparcv9/map.below4G.
(Source: https://docs.oracle.com/cd/E19120-01/open.solaris/819-0690/chapter6-34713/index.html)
Now I am aware the information from that link is awfully specific, but I was nonetheless wondering if there could be some more universal truth to this on other platforms, or at least point me in the right direction.
So I wrote a tiny test program in C and compiled it in two different ways:
gcc test.c - ELF type is ET_DYN / shared object file and no default virtual address offset is used for the LOAD segments:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x0005c8 0x0005c8 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x0001c5 0x0001c5 R E 0x1000
LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x000130 0x000130 R 0x1000
LOAD 0x002df0 0x0000000000003df0 0x0000000000003df0 0x000220 0x000228 RW 0x1000
gcc -static test.c - ELF type is ET_EXEC / executable and default virtual address offset of 0x400000 is used:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000518 0x000518 R 0x1000
LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x0936dd 0x0936dd R E 0x1000
LOAD 0x095000 0x0000000000495000 0x0000000000495000 0x02664d 0x02664d R 0x1000
LOAD 0x0bc0c0 0x00000000004bd0c0 0x00000000004bd0c0 0x005170 0x0068c0 RW 0x1000
Any ideas why that is? I know it possibly has to do with position-independent code, but I do not understand the necessity for an offset if absolute code is used (as in 2. above). Thanks.
When I change the start address to 0x10000 (PAGESIZE * 16), then the segmentation fault disappears. Any ideas why that is?
This was mentioned in the comments to this answer:
Why does loading at 0x10000 work but at 0x1000 doesn't? Does this depend on the kernel or the hardware? How do I pick the right number here?
Some code in the kernel doesn't like to use addresses below 0x10000, but I have not found that code.
I've tried to load a binary with first PT_LOAD.p_vaddr == 0x1000 into UML kernel (which is easy to debug), but that actually worked, so specific kernel code which prohibits this may be architecture-dependent.
I'd like to use the bpftool prog load to load my program into kernel. However, some errors occurred.
# bpftool prog load sockmap_update_kern.o "/sys/fs/bpf/bpf_sockmap"
libbpf: sec 'sockops': failed to find program symbol at offset 0
Error: failed to open object file
The program compiles fine with LLVM version 6.0.0
#include <linux/bpf.h>
#include "bpf_helpers.h"
struct bpf_map_def SEC("maps") sock_map = {
.type = BPF_MAP_TYPE_SOCKMAP,
.key_size = sizeof(int),
.value_size = sizeof(int),
.max_entries = 10,
};
SEC("sockops")
int sock_map_update(struct bpf_sock_ops *ops)
{
__u32 op, family;
int key;
op = ops->op;
family = ops->family;
switch (op){
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:
if (family == 2){ //AF_INET
key = 1;
bpf_sock_map_update(ops, &sock_map, &key, BPF_ANY);
}
default:
break;
}
return 0;
}
char _license[] SEC("license") = "GPL";
# readelf -a sockmap_update_kern.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Linux BPF
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 456 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 8
Section header string table index: 1
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .strtab STRTAB 0000000000000000 00000170
0000000000000051 0000000000000000 0 0 1
[ 2] .text PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 AX 0 0 4
[ 3] sockops PROGBITS 0000000000000000 00000040
0000000000000088 0000000000000000 AX 0 0 8
[ 4] .relsockops REL 0000000000000000 00000160
0000000000000010 0000000000000010 7 3 8
[ 5] maps PROGBITS 0000000000000000 000000c8
000000000000001c 0000000000000000 WA 0 0 4
[ 6] license PROGBITS 0000000000000000 000000e4
0000000000000004 0000000000000000 WA 0 0 1
[ 7] .symtab SYMTAB 0000000000000000 000000e8
0000000000000078 0000000000000018 1 2 8
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
There are no section groups in this file.
There are no program headers in this file.
There is no dynamic section in this file.
Relocation section '.relsockops' at offset 0x160 contains 1 entry:
Offset Info Type Sym. Value Sym. Name
000000000058 000300000001 unrecognized: 1 0000000000000000 sock_map
The decoding of unwind sections for machine type Linux BPF is not currently supported.
Symbol table '.symtab' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000078 0 NOTYPE LOCAL DEFAULT 3 LBB0_3
2: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 6 _license
3: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 5 sock_map
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 3 sock_map_update
No version information found in this file.
My kernel version is 5.3.0-42. I get it by apt-get install, so I guess there are some problems about the kernel. Could you give me some advise? Thanks in advance.
# uname -a
Linux iZ2zehe0r5ccv5sse5ib5fZ 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
I am on a quest to understand low-level computing. I have noticed my compiled binaries are a lot bigger then I think they should be. So I tried to build the smallest possible c program without any stdlib code as follows:
void _start()
{
while(1) {};
}
gcc -nostdlib -o minimal minimal.c
When I disasseble the binary, it shows me exactly what I expect, namely this exact code in three lines of assembly.
$ objdump -d minimal
minimal: file format elf64-x86-64
Disassembly of section .text:
0000000000001000 <_start>:
1000: 55 push %rbp
1001: 48 89 e5 mov %rsp,%rbp
1004: eb fe jmp 1004 <_start+0x4>
But my actual executable is still 13856 Bytes in size. What is it, that makes this so large? What else is in that file? Does the OS need more than these 6 Bytes of machine code?
Edit #1:
The output of size is:
$ size -A minimal
minimal :
section size addr
.interp 28 680
.note.gnu.build-id 36 708
.gnu.hash 28 744
.dynsym 24 776
.dynstr 1 800
.text 6 4096
.eh_frame_hdr 20 8192
.eh_frame 52 8216
.dynamic 208 16176
.comment 18 0
Total 421
Modern compilers and linkers aren't really optimized for producing ultra-small code on full-scale platforms. Not because the job is difficult, but because there's usually no need to. It isn't necessarily that the compiler or linker adds additional code (although it might), but rather that it won't try hard to pack your data and code into the smallest possible space.
In your case, I note that you're using dynamic linking, even though nothing is actually linked. Using "-static" will shave off about 8kB. "-s" (strip) will get rid of a bit more.
I don't know if it's even possible with gcc to make a truly minimal ELF executable. In your case, that ought to be about 400 bytes, nearly all of which will be the various ELF headers, section table, etc.
I don't know if I'm allowed to link my own website (I'm sure somebody will put me right if not), but I have an article on producing a tiny ELF executable by building it from scratch in binary:
http://kevinboone.me/elfdemo.html
There are many different executable file formats. .com, .exe, .elf, .coff, a.out, etc. They ideally contain the machine code and other sections (.text (code), .data, .bss, .rodata and possibly others, names depend on toolchain) plus they contain debugging information. Notice how your disassembly showed the label _start? that is a string among others and other info to be able to connect that string to the address for debugging. The output of objdump also showed that you are using an elf file, you can easily look up the file format and can trivially write your own program to parse through the file, or try to use readelf and other tools to see what is in there (high level not raw).
On an operating system where in general (not always, but think pc) the programs are being loaded into ram and then run, so you want to have first and foremost a file format that is supported by the operating system, there is no reason for them to support more than one, but they might. It is os/system design dependent, but the os may be designed to not only load the code, but also load/initialize the data (.data, .bss). When booting say an mcu you need to embed the data into the binary blob and the application itself copies the data to ram from the flash, but in an os that isn't necessarily required, but in order to do it you need a file format that can distinguish the sections, target locations, and sizes. Which means extra bytes in the file to define this and a file format.
A binary includes the bootstrap code before it can enter the C generated code, depending on the system, depending on the C library (multiple/many C libraries can be used on a computer and bootstrap is specific to the C library in general not the target, nor operating system, not a compiler thing), so some percentage of the file is the bootstrap code, too when your main program is very tiny the a lot of the file size is overhead.
You can for example use strip to make the file smaller by getting rid of some symbols and other non-essential items like that the file size should get smaller but the objdump disassembly will then not have labels and for the case of x86, a variable length instruction set which is difficult at best to disassemble gets much harder, so the output with or without labels may not reflect the actual instructions, but without the labels the gnu disassembler doesn't reset itself at the labels and can make the output worse.
If you use clang 10.0 and lld 10.0 and strip out unnecessary sections you can get the size of a 64-bit statically linked executable to under 800 bytes.
$ cat minimal.c
void _start(void)
{
int i = 0;
while (i < 11) {
i++;
}
asm( "int $0x80" :: "a"(1), "b"(i) );
}
$ clang -static -nostdlib -flto -fuse-ld=lld -o minimal minimal.c
$ ls -l minimal
-rwxrwxr-x 1 fpm fpm 1376 Sep 4 17:38 minimal
$ readelf --string-dump .comment minimal
String dump of section '.comment':
[ 0] Linker: LLD 10.0.0
[ 13] clang version 10.0.0 (Fedora 10.0.0-2.fc32)
$ readelf -W --section-headers minimal
There are 9 section headers, starting at offset 0x320:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000200190 000190 000018 00 A 0 0 4
[ 2] .eh_frame_hdr PROGBITS 00000000002001a8 0001a8 000014 00 A 0 0 4
[ 3] .eh_frame PROGBITS 00000000002001c0 0001c0 00003c 00 A 0 0 8
[ 4] .text PROGBITS 0000000000201200 000200 00002a 00 AX 0 0 16
[ 5] .comment PROGBITS 0000000000000000 00022a 000040 01 MS 0 0 1
[ 6] .symtab SYMTAB 0000000000000000 000270 000048 18 8 2 8
[ 7] .shstrtab STRTAB 0000000000000000 0002b8 000055 00 0 0 1
[ 8] .strtab STRTAB 0000000000000000 00030d 000012 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
$ strip -R .eh_frame_hdr -R .eh_frame minimal
$ strip -R .comment -R .note.gnu.build-id minimal
strip: minimal: warning: empty loadable segment detected at vaddr=0x200000, is this intentional?
$ readelf -W --section-headers minimal
There are 3 section headers, starting at offset 0x240:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000000201200 000200 00002a 00 AX 0 0 16
[ 2] .shstrtab STRTAB 0000000000000000 00022a 000011 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
$ ll minimal
-rwxrwxr-x 1 fpm fpm 768 Sep 4 17:45 minimal
I have been trying to learn about x86-64 machine code and ELF files. For that purpose i wrote some code to generate an ELF file with some machine code in it. I use a some machine code that i assembled using nasm (it just prints a message and calls the exit syscall, learning to assemble machine code myself comes next) and wrote a C program to write the correct ELF header/Section headers/Symbol table etc. manually into a file.
Now I am trying to link my file (with a single function in it) against another elf file, which I generate via gcc from C code (test.c):
// does not work with or without "extern"
extern void hello();
void _start()
{
hello();
// exit system call
asm(
"movl $60,%eax;"
"xorl %ebx,%ebx;"
"syscall");
}
The output of readelf -a on my ELF file is (hello.o):
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 64 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 9
Section header string table index: 8
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000280
0000000000000044 0000000000000000 AX 0 0 16
[ 2] .rela.text RELA 0000000000000000 000002c8
0000000000000030 0000000000000018 I 6 1 8
[ 3] .data PROGBITS 0000000000000000 00000300
0000000000000005 0000000000000000 WA 0 0 16
[ 4] .bss NOBITS 0000000000000000 00000310
0000000000000080 0000000000000000 A 0 0 16
[ 5] .rodata PROGBITS 0000000000000000 00000310
000000000000000d 0000000000000000 A 0 0 16
[ 6] .symtab SYMTAB 0000000000000000 00000320
0000000000000150 0000000000000018 7 14 8
[ 7] .strtab STRTAB 0000000000000000 00000470
0000000000000028 0000000000000000 0 0 1
[ 8] .shstrtab STRTAB 0000000000000000 00000498
000000000000003f 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
There are no section groups in this file.
There are no program headers in this file.
There is no dynamic section in this file.
Relocation section '.rela.text' at offset 0x2c8 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000001a 000500000001 R_X86_64_64 0000000000000000 .rodata + 0
000000000024 00050000000a R_X86_64_32 0000000000000000 .rodata + d
The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 8
9: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
10: 0000000000000000 68 FUNC GLOBAL DEFAULT 1 hello
11: 0000000000000060 13 OBJECT LOCAL DEFAULT 5 msg
12: 000000000000000d 8 NOTYPE LOCAL DEFAULT ABS len
13: 0000000000000050 5 OBJECT GLOBAL DEFAULT 3 _test
No version information found in this file.
I have compiled test.c with
gcc -c -nostdlib -fno-asynchronous-unwind-tables test.c -o test.o
to then link with ld test.o hello.o, which unfortunately yields
ld: test.o: in function `_start':
test.c:(.text+0xa): undefined reference to `hello'
even though the hello function is defined in hello.o (note the entry in the symbol table named hello which is in section 1, the .text section, and seems to have the correct size/type/value/bind).
If I compile a file with just void hello(){} in it the same way I compiled test.c, those two object files can obviously be linked. Also, if I generate my own ELF file hello.o as an executable, renaming the hello function to _start it executes just fine. I have been banging my head against the Wall for a while now, and there is two things I would like to know: Obviously I would like to know my issue with the ELF file. But also I would like to know how I can debug such issues in the future. I have tried to build ld from source (cloning the GNU binutils repo) with debugging symbols, but I did not get very far debugging ld itself.
Edit: I have uploaded my elf file here:
https://drive.google.com/file/d/1cRNr0VPAjkEbueuWFYwLYbpijVnLySqq/view?usp=sharing
This was quite hard to debug.
Here is the output from readelf -WSs hello.o for the file you uploaded to Google drive (it doesn't match the info in your question):
There are 9 section headers, starting at offset 0x40:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000000000000 000280 000044 00 AX 0 0 16
[ 2] .rela.text RELA 0000000000000000 0002c8 000030 18 I 6 1 8
[ 3] .data PROGBITS 0000000000000000 000300 000005 00 WA 0 0 16
[ 4] .bss NOBITS 0000000000000000 000310 000080 00 A 0 0 16
[ 5] .rodata PROGBITS 0000000000000000 000310 00000d 00 A 0 0 16
[ 6] .symtab SYMTAB 0000000000000000 000320 000150 18 7 14 8
[ 7] .strtab STRTAB 0000000000000000 000470 000028 00 0 0 1
[ 8] .shstrtab STRTAB 0000000000000000 000498 00003f 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 8
9: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
10: 0000000000000000 68 FUNC GLOBAL DEFAULT 1 hello
11: 0000000000000060 13 OBJECT LOCAL DEFAULT 5 msg
12: 000000000000000d 8 NOTYPE LOCAL DEFAULT ABS len
13: 0000000000000050 5 OBJECT GLOBAL DEFAULT 3 _test
The issue is with the .sh_info value (14) of the .symtab section.
According to documentation, .sh_info for SYMTAB section is supposed to contain "one greater than the symbol table index of the last local symbol (binding STB_LOCAL)."
So the value 14 tells the linker that all symbols in this file are local, and therefore can't possibly be used to resolve any external references to them.
You need to move all LOCAL symbols before GLOBAL ones (here, msg and len would need to move before hello), so that the symbol table looks like this:
...
9: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
10: 0000000000000060 13 OBJECT LOCAL DEFAULT 5 msg
11: 000000000000000d 8 NOTYPE LOCAL DEFAULT ABS len
12: 0000000000000000 68 FUNC GLOBAL DEFAULT 1 hello
13: 0000000000000050 5 OBJECT GLOBAL DEFAULT 3 _test
and then set .sh_info for the .symtab section to 12.
But also I would like to know how I can debug such issues in the future.
As you've discovered, debugging binutils ld is very hard, partially because it uses libbfd, which is choke-full of macros and is itself very hard to debug.
I debugged this by building Gold from source, which fortunately produced the exact same failure.
I have a tool emitting an ELF, which as far as I can tell is compliant to the spec. Readelf output looks fine, but objdump refuses to disassemble anything.
I have simplified the input to a single global var, and "int main(void) { return 0;}" to aid debugging - the tiny section sizes are correct.
In particular, objdump seems unable to find the sections table:
$ arm-none-linux-gnueabi-readelf -S davidm.elf
There are 4 section headers, starting at offset 0x74:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text NULL ff000000 000034 00001c 00 AX 0 0 4
[ 2] .data NULL ff00001c 000050 000004 00 WA 0 0 4
[ 3] .shstrtab NULL 00000000 000114 000017 00 0 0 0
$ arm-none-linux-gnueabi-objdump -h davidm.elf
davidm.elf: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
I also have another ELF, built from the exact same objects, only produced with regular toolchain use:
$ objdump -h kernel.elf
kernel.elf: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001c ff000000 ff000000 00008000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 ff00001c ff00001c 0000801c 2**2
CONTENTS, ALLOC, LOAD, DATA
Even after I stripped .comment and .ARM.attributes sections (incase objdump requires them) from the 'known good' kernel.elf, it still happily lists the sections there, but not in my tool's davidm.elf.
I have confirmed the contents of the sections are identical between the two with readelf -x.
The only thing I can image is that the ELF file layout is different and breaks some expectations of BFD, which could explain why readelf (and my tool) processes it just fine but objdump has troubles.
Full readelf:
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0xff000000
Start of program headers: 84 (bytes into file)
Start of section headers: 116 (bytes into file)
Flags: 0x5000002, has entry point, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 40 (bytes)
Number of section headers: 4
Section header string table index: 3
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text NULL ff000000 000034 00001c 00 AX 0 0 4
[ 2] .data NULL ff00001c 000050 000004 00 WA 0 0 4
[ 3] .shstrtab NULL 00000000 000114 000017 00 0 0 0
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000034 0xff000000 0xff000000 0x00020 0x00020 RWE 0x8000
Section to Segment mapping:
Segment Sections...
00 .text .data
There is no dynamic section in this file.
There are no relocations in this file.
There are no unwind sections in this file.
No version information found in this file.
Could the aggressive packing of the on-disk layout be causing troubles? Am I in violation of some bytestream alignment restrictions BFD expects, documented or otherwise?
Lastly - this file is not intended to be mmap'd into an address space, a loader will memcpy segment data into the desired location, so there is no requirement to play mmap-friendly file-alignment tricks. Keeping the ELF small is more important.
Cheers,
DavidM
EDIT: I was asked to upload the file, and/or provide 'objdump -x'. So I've done both:
davidm.elf
$ objdump -x davidm.elf
davidm.elf: file format elf32-littlearm
davidm.elf
architecture: arm, flags 0x00000002:
EXEC_P
start address 0xff000000
Program Header:
LOAD off 0x00000034 vaddr 0xff000000 paddr 0xff000000 align 2**15
filesz 0x00000020 memsz 0x00000020 flags rwx
private flags = 5000002: [Version5 EABI] [has entry point]
Sections:
Idx Name Size VMA LMA File off Algn
SYMBOL TABLE:
no symbols
OK - finally figured it out.
After building and annotating/debugging libbfd (function elf_object_p()) in the context of a little test app, I found why it was not matching on any of BFD supported targets.
I had bad sh_type flags for the section headers: NULL. Emitting STRTAB or PROGBITS (and eventually NOBITS when I get that far) as appropriate and objdump happily walks my image.
Not really surprising, in retrospect - I'm more annoyed I didn't catch this in comparing readelf outputs than anything else :(
Thanks for the help all :)