📜 ⬆️ ⬇️

Minimal program in ELF format

Inspired by the article Hi from the libc free world , I also decided to do something similar. In order not to do this aimlessly, I decided to set myself the following task. Make a program that outputs some simple line, like "ELF, hello!". Understand exactly how it will be represented in the executable file. Well, along the way, try to keep within 100 bytes.

For starters, the standard helloworld in C ++

#include <iostream> using namespace std; int main() { cout << "ELF, hello!\n"; return 0; } 

Compile, look at the size:

 $ g++ test.cpp -static && ls -s -h a.out 1,3M a.out 

')
How much how much 1.3 MB? To output one single message size of 12 bytes? Hmm ... Okay, let's try Si.

 #include "stdio.h" int main() { printf("ELF, hello!\n"); return 0; } 

We also compile it. When compiling, I specified the option -static - I’m interested in the whole code, which will be executed. With dynamic compilation, the dimensions are certainly smaller, but still not as much as we would like.

 $ gcc test.c -static && ls -s -h a.out 568K a.out 


On the floor a megabyte less. Here it is, the fee for STL. But still a lot. Apparently, heavy artillery in the form of an assembler is indispensable. We write helloworld on asm, and without stdlib. I prefer the AT & T syntax.

 .data str: .ascii "ELF, hello!" .byte 10 .text .global _start _start: movl $4, %eax movl $1, %ebx movl $str, %ecx movl $12, %edx int $0x80 movl $1, %eax movl $0, %ebx int $0x80 


Two sections, in the data section - our message (and 10-ka for a new line), in the code section (.text) - we call the 80th interrupt twice (with the necessary parameters in the registers), the first time to display a message, second time for correct completion.

We compile (or rather, broadcast and link) the created program:

 $ gcc easy.s -nostdlib && du -sb a.out 752 a.out 


752 bytes - this is already much closer to what is required. Remove the debugging symbols with the strip utility:

 $ strip a.out && du -sb a.out 476 a.out 


Better, but still not enough. What is in our file by as much as 476 bytes? Disassemble a.out using objdump:

 $ objdump -D a.out a.out: file format elf32-i386 Disassembly of section .note.gnu.build-id: 08048094 <.note.gnu.build-id>: 8048094: 04 00 add $0x0,%al ... - ,     ... 80480b6: b6 08 mov $0x8,%dh Disassembly of section .text: 080480b8 <.text>: 80480b8: b8 04 00 00 00 mov $0x4,%eax 80480bd: bb 01 00 00 00 mov $0x1,%ebx 80480c2: b9 dc 90 04 08 mov $0x80490dc,%ecx 80480c7: ba 0c 00 00 00 mov $0xc,%edx 80480cc: cd 80 int $0x80 80480ce: b8 01 00 00 00 mov $0x1,%eax 80480d3: bb 00 00 00 00 mov $0x0,%ebx 80480d8: cd 80 int $0x80 Disassembly of section .data: 080490dc <.data>: 80490dc: 45 inc %ebp 80490dd: 4c dec %esp 80490de: 46 inc %esi 80490df: 2c 20 sub $0x20,%al 80490e1: 68 65 6c 6c 6f push $0x6f6c6c65 80490e6: 21 0a and %ecx,(%edx) 


And so, we see three sections, although we wrote only two. In the .text section is our code. In the data section, our elf hello is in the form of 12 bytes (objdump also disassembles them). And what about the .note.gnu.build-id section? We did not order it, so feel free to delete:

 $ strip -R .note.gnu.build-id a.out && du -sb a.out 416 a.out 


Won another 60 bytes. Not bad. Let's try to optimize our code a bit. First, the program can in principle be terminated with any code, and not necessarily with zero. Secondly, when you start the program, the registers are reset (however, you should not rely on it when creating real programs - check the ABI of the system for which you are writing).
As a result, instead of movl $ 4,% eax, which is translated to 5 bytes, we can use movb $ 4,% al, which are translated to 2 bytes. Thirdly, we’ll get rid of the .data section by placing our line in the code after the last interruption (the program is no longer running anyway):

 .text .global _start _start: movb $4, %al movb $1, %bl movl $str, %ecx movb $12, %dl int $0x80 movb $1, %al int $0x80 str: .ascii "ELF, hello!" .byte 10 


Compile, delete too much, look at the size:

 $ gcc -nostdlib easy.s $ strip a.out $ strip -R .note.gnu.build-id a.out $ du -sb a.out 320 a.out 


It seems we have reached the limit. 320 bytes is nothing superfluous. Or not? Where do these 320 bytes come from? Our code is clearly smaller. However, besides the code in our binary file there is also an ELF header. And if we want to make a truly minimal program, we will have to open the ELF description (for example, here ), and form the title manually.

Manually - this does not mean in the hex editor. You can simply make it clear to the linker that we don’t need to attribute anything to our file, and it will output exactly what we write at the output. True, in this case, the entire responsibility for ensuring that the file starts to fall on us.
The implementation of the program with a manually compiled header I got this:

  .set ofs, 0x10000 /* ofs -    */ /* ELF : */ .byte 0x7F .ascii "ELF" .long 0, 0, 0 /* ident */ .word 2 /* type */ .word 3 /* machine */ .long 0 /* version */ .long _start + ofs /* entry -    () */ .long phdr /* phoff -    (phdr) ( ) */ .long 0 /* shoff */ .long 0 /* flags */ .word 0 /* ehsize -  elf  */ .word phdrsize /* phentsize -  .  */ .word 1 /* phnum -  . . */ .word 0 /* shentsize */ .word 0 /* shnum */ .word 0 /* e_shstrndx */ /*   */ phdr: .long 1 /* type */ .long 0 /* offset */ .long ofs /* vaddr -      (  ) */ .long 0 /* paddr */ .long filesize /* filesz -     */ .long filesize /* memsz -     */ .long 5 /* pflags */ .long 0 /* palign */ .set phdrsize, . - phdr _start: /*   */ movb $4, %al movb $1, %bl movl $(str+ofs), %ecx movb $12, %dl int $0x80 movb $1, %al int $0x80 str: .ascii "ELF, hello!" .byte 10 .set filesize, . 


Now we also have to manually operate with the program offset. By displacement, simply, you can understand the difference in addressing between the code that lies in our program and where it will be placed in RAM (in fact, in RAM, it will not lie there, but that's another story). Usually the linker is involved in determining the required offsets, but now we are on our own. The offset I placed in the ofs parameter. The size of the offset took the minimum possible on my machine (10,000). By default, it is 8048000, but this is not a prerequisite.

The ELF header itself is not really one ELF header. There should be at least two of them - the elf header, and the program header. In general, there are more section headers, but we will not use them to save space. Empirically, header fields that are used have been established. The rest were filled with zeros.

We are broadcasting the program, this time manually calling as and ld:

 $ as w3test.s -o w3test.o $ ld -Ttext 0 --oformat binary -o w3test w3test.o $ du -sb w3test 115 w3test 


115 bytes! ~ 10,000 times smaller than the original version. It would seem that everything. There is only the minimum necessary to run, and nothing more. And the initial task to overcome 100 bytes will not succeed. However, this is not the limit! There are unused bytes in the header, which means that we can use them for our purposes. The code itself unfortunately will not fit into any field, it is too big. But the string will fit.

If you look closely, then immediately after the ELF identifier we have three unused long fields (four bytes each). This means that we can put a string there. And besides, not the whole line, but only the last part of it, because we already have ELF in the form of ascii characters.

In addition, we can reduce the code by placing the phd header not after elf, but immediately after the last byte used in ELF. That is, the phd header will be slightly layered on elf, but this will not cause any consequences, since those fields that are layered are not used in elf.
In the same way, we can place our program by “layering” it on the phd header (for the same reasons).

The result is the following code:

  .set ofs, 0x10000 /* ofs -    */ /* ELF : 8*/ .byte 0x7F str: .ascii "ELF" .ascii ", hello!" .byte 10, 0, 0, 0 .word 2 /* type */ .word 3 /* machine */ .long 0 /* version */ .long _start+ofs /* entry -    () */ .long phdr /* phoff -    (phdr) ( ) */ .long 0 /* shoff */ .long 0 /* flags */ .word 0 /* ehsize -  elf  */ .word phdrsize /* phentsize -  .  */ /*   */ phdr: .long 1 /* type */ .long 0 /* offset */ .long ofs /* vaddr -      (  ) */ .long 0 /* paddr */ .long filesize /* filesz -     */ .long filesize /* memsz -     */ .long 5 /* pflags */ .set phdrsize, . - phdr + 4 _start: /*   */ movb $4, %al movb $1, %bl movl $(str+ofs), %ecx movb $12, %dl int $0x80 movb $1, %al int $0x80 .set filesize, . 


After the broadcast, we get a program of 89 bytes. You can consider the task completed.

There was also an optimization idea - to put the phd header inside the elf header. But this idea failed, since the minimum offset of 10,000 did not make it possible to select such parameters so that the required fields of the structures coincided.

PS In the comments, an even more optimized version was proposed, 61 bytes in size, in which we managed to impose phd on elf. Compiled using nasm / yasm with the -f bin option.

 BITS 32; ORG 05430000h; DB 0x7F, "ELF"; DD 01h, 00h, $$; DW 02h, 03h; DD @main; DW @main - $$; @main: INC EBX; DB 05h; <-- ADD EAX, DD 04h; <-- LONG(04h) MOV ECX, @text; MOV DL, 12; INT 80h; AND EAX, 00010020h; XCHG EAX, EBX; INT 80h; @text: DB "ELF, hello!", 0Ah; 


Information sources


wikibooks.org - Linux Assembler for C Programmers
stackoverflow.com - “Hello World” in less than 20 bytes
muppetlabs.com - Teensy ELF Executables for Linux

Source: https://habr.com/ru/post/137706/


All Articles