This is a card in Dave's Virtual Box of Cards.

i386 Assembly Language trick for storing data in .text

Created: 2023-11-08
Updated: 2023-11-09

Don’t want a big old bloated multi-kilobyte executable with a data segment, but you do need to store some data?

I "discovered" this while working on Meow5.

One of my biggest challenges has been storing strings in a position-independent way. I’ve considered a Global Offset Table (GOT), but it seems like way more machinery than I need. And though I went well out of my way to figure out how to make multi-segment ELF executables, I just couldn’t bear to export a "huge" 2kb+ padded file with the proper alignment.

So I was determined to store strings in a Forth-style manner - embedding them with the executable code and jumping over them.

To do this was going to be a lot of painful trial-and-error to get the exact right opcodes compiled in place to push the address of the string on the stack followed by a jmp instruction to hop over the string (gotta know the length first, then come back and write the instruction).

The thing that makes this extra hard is that 32-bit i386 doesn’t have an instruction for getting the value of the instruction pointer (EIP) directly. (x86_64 added this later.)

But I realized something: there is an instruction that pushes the following address on the stack and then jumps to a location. It’s call.

So I made a little stand-alone NASM assembly "Hello world" test as a proof of concept. I realized I can even get the length of the string by subtracting the offset of my call’d label from the return address:

global _start
_start:

    ; call pushes the next address on the stack
    ; so we could "return" there
    call print

    ; this gets jumped over, but we've got the address!
    db `Hello World!\n`

print:
    ; Print with Linux SYS_WRITE
    ; pop the address from the stack to ecx
    pop ecx        ; string address
    mov edx, print ; label address
    sub edx, ecx   ; length of string!
    mov     ebx, 1 ; STDOUT
    mov     eax, 4 ; SYS_WRITE
    int     80h

    ; Exit with Linux SYS_EXIT
    mov ebx, 0 ; return status
    mov eax, 1 ; SYS_EXIT
    int 80h    ; kernel syscall

Let’s build it:

$ nasm -f elf32 -o hello.o hello.asm
$ ld -m elf_i386 -n hello.o -o hello

Without a data segment, this executable is truly tiny, just 532 bytes.

$ ls -l hello
-rwxr-xr-x 1 dave users 532 Nov  8 20:58 hello

And does it work?

$ ./hello
Hello World!

Of course. :-)

I’m sure this trick is well-known and has a name and everything, but it’s new to me.

I was really pleased with how nice and neat it turned out and it’s definitely going straight into Meow5, which should allow me to finally finish that project.

In Meow5 - Same thing is just 144 bytes!

It works! I adapted the trick using the opcode for a relative call instruction and the calculated string length (I have to leave a space for it and then "come back" and write the instruction after I know the string length).

Compilation in Meow5 happens as you type the program.

A word (function) called make_elf exports a word (function) as a stand-alone executable:

Here you see three things:

  1. I make a word (function) called hello that prints "Hello World!" and exits (also exits the interpreter!)

  2. I write that word as a stand-alone executable 32-bit ELF program

  3. I run the word in the interpreter - it prints Hello World and exits the interpreter!

$ ./meow5
: hello "Hello World!\n" print exit ;

make_elf hello
Wrote to "hello".

hello
Hello World!

Okay, so let’s take a look at that executable. First of all, it’s tiny. Nothing but the minimum required ELF header followed by the exact instructions needed to print and exit.

$ ls -l hello
-rwxr-xr-x 1 dave users 144 Nov  9 20:15 hello

And of course we need to see it working:

$ ./hello
Hello World!

Finally, I’ll show what the ELF file looks like with my mez program! (I cheated and renamed hello to foo because my mez tool only examines a file called foo, ha ha):

+-[ELF Header]------------------------------------------+
| 7F E  L  F  01 01 01 00 00 00 00 00 00 00 00 00 02 00 |
| 03 00 01 00 00 00 54 80 04 08 34 00 00 00 00 00 00 00 |
| 00 00 00 00 34 00 20 00 01 00 00 00 00 00 00 00 /-----+
+--+---------------------------------------------/
   +-- Entry point address: 0x08048054
   +-- Program header file offset: 0x34
   +-- Program header size: 32 (0x20)
   \-- Program header count: 1
    |
    +-- Program Header 0 at 0x34, type: PT_LOAD

+-[Program Header 0]------------------------------+
| 01 00 00 00 00 00 00 00 00 80 04 08 00 80 04 08 |
| 90 00 00 00 90 00 00 00 07 00 00 00 00 00 00 00 |
+--+----------------------------------------------+
   +-- File data start offset: 0x0
   +-- File data bytes to load: 144 (0x90)
   +-- Memory segment start addr: 0x08048000
   +-- Memory segment byte size: 144 (0x90)
   +-- Memory segment flags: R+W+X (0x7)
   +-- Contains entry point 0x08048054
from 8048000 to 8048090...
0x08048000 7f 45 4c 46 01 01 01 00 00 00 00 00 .ELF........
0x0804800c 00 00 00 00 02 00 03 00 01 00 00 00 ............
0x08048018 54 80 04 08 34 00 00 00 00 00 00 00 T...4.......
0x08048024 00 00 00 00 34 00 20 00 01 00 00 00 ....4.......
  ...Skipping to entry point...
0x08048054 e8 0e 00 00 00 48 65 6c 6c 6f 20 57 .....Hello.W
0x08048060 6f 72 6c 64 21 0a 00 58 50 50 58 b9 orld!..XPPX.
0x0804806c 00 00 00 00 80 3c 08 00 74 03 41 eb .....<..t.A.
0x08048078 f7 51 5a 59 bb 01 00 00 00 b8 04 00 .QZY........
  ...12 more bytes to load...