I’m in love with Forth but there are no commercial Forth environments for Mac OSX. GForth is a free, fast and portable implementation of ANS Forth but it requires GCC and does not allow for binary distribution of code that uses foreign functions.
There are two excellent commercial implementations of ANS Forth and both run on Linux. I asked one of the companies if I could port their Forth to the Mac and promptly ended up with a tarball on my lap. There were no C or assembler files, it was all Forth source code.
The proper bootstrapping approach turned out to generate a Mac kernel on Linux, copy it over to the Mac and use it to compile the rest of the Forth environment. It’s called cross-compiling!
This required me to investigate how Mac binaries are laid out and how I could generate them without using gcc or a linker.
I would like to explain how I did it. Let’s start with a simple C program and feel free to browse the full source code.
1 #include
2 #include
3
4 int main(int argc, char **argv)
5 {
6 printf("Hello world!\n");
7 exit(0);
8 }
It can’t get any simpler!
1 gcc hello.c -o hello
2 ./hello
3 Hello world!
What does it look like in assembler, though?
1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world! 1 .cstring
2 LC0:
3 .ascii "Hello world![[posterous_whitelist_block_2]]"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
"
4 .text
5 .globl _main
6 _main:
7 pushl %ebp
8 movl %esp, %ebp
9 pushl %ebx
10 subl $20, %esp
11 call L3
12 "L00000000001$pb":
13 L3:
14 popl %ebx
15 leal LC0-"L00000000001$pb"(%ebx), %eax
16 movl %eax, (%esp)
17 call L_puts$stub
18 movl $0, (%esp)
19 call L_exit$stub
20 .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
21 L_exit$stub:
22 .indirect_symbol _exit
23 hlt ; hlt ; hlt ; hlt ; hlt
24 L_puts$stub:
25 .indirect_symbol _puts
26 hlt ; hlt ; hlt ; hlt ; hlt
27 .subsections_via_symbols
The IMPORT section is where gcc allocates stubs for external functions. The dynamic linker will replace these with a jump to the real printf once libc is loaded.
What the code above does not include is proper alignment of the stack before the calls to printf and exit. This is required according to the Mac OSX ABI IA-32 Function Calling Conventions. It’s a slight of hand on the part of gcc which inserts a prolog before invoking our main function.
This prolog sets up the stack and gets hold of our program arguments, i.e. argc, argv and envp.
1 Breakpoint 1, 0x00001f6c in start ()
2 (gdb) disas
3 Dump of assembler code for function start:
4 0x00001f68 : push $0x0
5 0x00001f6a : mov %esp,%ebp
6 0x00001f6c : and $0xfffffff0,%esp ; <-- stack alignment
7 0x00001f6f : sub $0x10,%esp ; <-- and here too!
8 0x00001f72 : mov 0x4(%ebp),%ebx
9 0x00001f75 : mov %ebx,0x0(%esp)
10 0x00001f79 : lea 0x8(%ebp),%ecx
11 0x00001f7c : mov %ecx,0x4(%esp)
12 0x00001f80 : add $0x1,%ebx
13 0x00001f83 : shl $0x2,%ebx
14 0x00001f86 : add %ecx,%ebx
15 0x00001f88 : mov %ebx,0x8(%esp)
16 0x00001f8c : mov (%ebx),%eax
17 0x00001f8e : add $0x4,%ebx
18 0x00001f91 : test %eax,%eax
19 0x00001f93 : jne 0x1f8c
20 0x00001f95 : mov %ebx,0xc(%esp)
21 0x00001f99 : call 0x1fca
22 0x00001f9e : mov %eax,0x0(%esp)
23 0x00001fa2 : call 0x3000
24 0x00001fa7 : hlt
25 End of assembler dump.
Let’s tidy things up into a single NASM file. It’s less verbose than GAS and I much prefer it.
1 bits 32
2
3 section .text
4
5 GLOBAL start
6 extern _printf, _exit
7
8 start:
9 and esp, 0xFFFFFFF0
10 sub esp, 0x10
11 mov dword [esp], hello.msg
12 call _printf
13 add esp, 0x10
14 mov eax, 0 ; set return code
15 call _exit
16 hlt
17
18 section .data
19
20 hello.msg db 'Hello, World!', 0x0a, 0x00
The stubs are taken care of by nasm in Mach-O mode (-f macho below) and the code still works.
1 nasm -f macho hello.asm -o hello.o
2 ld hello.o -o hello -lc
3
4 ./hello
5 Hello, World!
otool is indispensable for any sort of involved Mac forensics and the Mach-O file format is very well explained by Apple.
1 otool -l hello
2 hello:
3 Load command 0
4 cmd LC_SEGMENT
5 cmdsize 56
6 segname __PAGEZERO
7 vmaddr 0x00000000
8 vmsize 0x00001000
9 fileoff 0
10 filesize 0
11 maxprot 0x00000000
12 initprot 0x00000000
13 nsects 0
14 flags 0x0
15 ...
16 Load command 8
17 cmd LC_UUID
18 cmdsize 24
19 uuid 0xce 0x2c 0xd0 0xae 0xbb 0x29 0xb4 0xc5
20 0xba 0x70 0x39 0x06 0x18 0x30 0x42 0x7b
21 Load command 9
22 cmd LC_UNIXTHREAD
23 cmdsize 80
24 flavor i386_THREAD_STATE
25 count i386_THREAD_STATE_COUNT
26 eax 0x00000000 ebx 0x00000000 ecx 0x00000000 edx 0x00000000
27 edi 0x00000000 esi 0x00000000 ebp 0x00000000 esp 0x00000000
28 ss 0x00000000 eflags 0x00000000 eip 0x00001fd0 cs 0x00000000
29 ds 0x00000000 es 0x00000000 fs 0x00000000 gs 0x00000000
30 Load command 10
31 cmd LC_LOAD_DYLIB
32 cmdsize 52
33 name /usr/lib/libSystem.B.dylib (offset 24)
34 time stamp 2 Thu Jan 1 01:00:02 1970
35 current version 111.1.3
36 compatibility version 1.0.0
The Mach-O header is normally generated by the compiler and the linker (GCC & LD) but I’m using neither so I have to generate the header by hand. It’s doable, as long as NASM is instructed to simply dump a binary image to disk (-f bin) and it actually works!
1 nasm -f bin hello1.asm -o hello1
2 chmod +x hello1
3 ./hello1
4 Hello, World!
Note that this can be done on any platform NASM runs on. I did it on Linux but assume it will work just as well on Windows.
Now, let’s take a good look at the code…
We need to tell NASM we are in 32-bit mode and that program code starts on the second VM page (0x1000 or 4096). The first page (PAGEZERO) is there to catch null pointer references.
1 ;;; File: hello1.asm
2 ;;; Build: nasm -f bin -o hello1 hello1.asm && chmod +x hello1
3
4 bits 32
5 org 0x1000
The header specifies that this is an x86-32 binary and a full-fledged executable file and that there are 10 load commands in the header.
1 mhdr:
2 dd 0xFEEDFACE ; magic
3 dd 7 ; cputype
4 dd 3 ; cpusubtype
5 dd 2 ; filetype
6 dd 10 ; ncmds
7 dd sizeofcmds ; sizeofcmds
8 dd 0x85 ; flags
PAGEZERO is where you end up when dereferencing a 0 pointer. This page is protected from reading and writing so any access to it causes a page fault and a memory access violation. This segment does not take any space in the file so its filesize is set to 0.
1 ;;; Load command #0
2
3 pagezero:
4 dd 1 ; LC_SEGMENT
5 dd _pagezero ; size
6 db '__PAGEZERO' ; segname
7 times 6 db 0 ; padding to 16 chars
8 dd 0 ; vmaddr
9 dd 0x1000 ; vmsize
10 dd 0 ; fileoff
11 dd 0 ; filesize
12 dd 0 ; maxprot
13 dd 0 ; initprot
14 dd 0 ; nsects
15 dd 0 ; flags
16 _pagezero equ $-pagezero
The text segment is where our code lives. It’s readable and executable (initprot). The load commands that form part of the Mach-O header itself need to be loaded somewhere. Here, they are part of the text segment which is why the segment starts at the beginning of the file (fileoff 0).
1 ;;; Load command #1
2
3 code:
4 dd 1 ; LC_SEGMENT
5 dd _code ; size
6 db '__TEXT' ; segname
7 times 10 db 0 ; padding to 16 chars
8 dd 0x1000 ; vmaddr
9 dd 0x1000 ; vmsize
10 dd 0 ; fileoff
11 dd 0x1000 ; filesize
12 dd 7 ; maxprot
13 dd 5 ; initprot
14 dd 1 ; nsects
15 dd 0 ; flags
16
17 sect1: ; section 0
18 db '__text' ; sectname
19 times 10 db 0 ; padding to 16 chars
20 db '__TEXT' ; segname
21 times 10 db 0 ; padding to 16 chars
22 dd start ; addr
23 dd codesize ; size
24 dd start-$$ ; offset
25 dd 0 ; align on 2^0
26 dd 0 ; reloff
27 dd 0 ; nreloc
28 dd 0x80000400 ; flags
29 dd 0 ; reserved1
30 dd 0 ; reserved2
31 _code equ $-code
The data segment holds our “Hello world!” string.
1 ;;; Load command #2
2
3 data:
4 dd 1 ; LC_SEGMENT
5 dd _data ; size
6 db '__DATA' ; segname
7 times 10 db 0 ; padding to 16 chars
8 dd 0x2000 ; vmaddr
9 dd 0x1000 ; vmsize
10 dd 0x1000 ; fileoff
11 dd 0x1000 ; filesize
12 dd 7 ; maxprot
13 dd 3 ; initprot
14 dd 1 ; nsects
15 dd 0 ; flags
16
17 sect2: ; section 0
18 db '__const' ; sectname
19 times 9 db 0 ; padding to 16 chars
20 db '__DATA' ; segname
21 times 10 db 0 ; padding to 16 chars
22 dd 0x2000 ; addr
23 dd 15 ; size, our string
24 dd 4096 ; offset
25 dd 0 ; align on 2^0
26 dd 0 ; reloff
27 dd 0 ; nreloc
28 dd 0 ; flags
29 dd 0 ; reserved1
30 dd 0 ; reserved2
31 _data equ $-data
The IMPORT segment holds our jump table, the stubs for printf and exit. The dynamic linker will fill in the stubs for us with a jump to printf and exit in libc. This segment needs to be readable, writable and executable (initprot).
1 ;;; Load command #3
2
3 stubs:
4 dd 1 ; LC_SEGMENT
5 dd _stubs ; size
6 db '__IMPORT' ; segname
7 times 8 db 0 ; padding to 16 chars
8 dd 0x3000 ; vmaddr
9 dd 0x1000 ; vmsize
10 dd 0x2000 ; fileoff
11 dd 0x1000 ; filesize
12 dd 7 ; maxprot
13 dd 7 ; initprot
14 dd 1 ; nsects
15 dd 0 ; flags
16
17 sect3: ; section 0
18 db '__jump_table' ; sectname
19 times 4 db 0 ; padding to 16 chars
20 db '__IMPORT' ; segname
21 times 8 db 0 ; padding to 16 chars
22 dd 0x3000 ; addr
23 dd 10 ; size, two stubs
24 dd 0x2000 ; offset
25 dd 6 ; align on 2^6
26 dd 0 ; reloff
27 dd 0 ; nreloc
28 dd 0x04000008 ; flags
29 dd 0 ; reserved1
30 dd 5 ; reserved2, stub size
31 _stubs equ $-stubs
The LINKEDIT segment holds the symbol table.
1 ;;; Load command #4
2
3 linkage:
4 dd 1 ; LC_SEGMENT
5 dd _linkage ; size
6 db '__LINKEDIT' ; link table
7 times 6 db 0 ; padding
8 dd 0x4000 ; vmaddr
9 dd 0x1000 ; vmsize
10 dd symbols-$$ ; fileoff
11 dd _symbols ; filesize
12 dd 7 ; maxprot
13 dd 1 ; initprot
14 dd 0 ; nsects
15 dd 0 ; flags
16 _linkage equ $-linkage
This segment describes our symbol table, including where the symbols and the strings naming them are located. I believe it’s mostly for the benefit of the debugger.
1 ;;; Load command #5
2
3 symtab:
4 dd 2 ; LC_SYMTAB
5 dd _symtab ; size
6 dd symbols-$$ ; symoff
7 dd 4 ; nsyms
8 dd strings-$$ ; stroff
9 dd _strings ; strsize
10 _symtab equ $-symtab
This load command describes the dynamic symbol table. This is how the dynamic linker knows to plug the stubs (indirect).
1 ;;; Load command #6
2
3 dysymtab:
4 dd 0x0b ; LC_DYSYMTAB
5 dd _dysymtab ; size
6 dd 0 ; ilocalsym
7 dd 1 ; nlocalsym
8 dd 1 ; iextdefsym
9 dd 2 ; nextdefsym
10 dd 2 ; iundefsym
11 dd 2 ; nundefsym
12 dd 0 ; tocoff
13 dd 0 ; ntoc
14 dd 0 ; modtaboff
15 dd 0 ; nmodtab
16 dd 0 ; extrefsymoff
17 dd 0 ; nextrefsyms
18 dd indirect-$$ ; indirectsymoff
19 dd 2 ; nindirectsyms
20 dd 0 ; extreloff
21 dd 0 ; nextrel
22 dd 0 ; locreloff
23 dd 0 ; nlocrel
24 _dysymtab equ $-dysymtab
My guess is as good as yours here. I’m not ready to use a dynamic linker of my own but this is a distinct possibility! This load command clearly provides for it.
1 ;;; Load command #7
2
3 dylinker:
4 dd 0x0e ; LC_LOAD_DYLINKER
5 dd _dylinker ; size
6 dd 12 ; nameoff
7 db '/usr/lib/dyld', 0
8 align 4
9 _dylinker equ $-dylinker
This load command specifies the contents of the registers at startup. I haven’t seen anything other than EIP populated, though. The program will not run unless this load command is present!
1 ;;; Load command #8
2
3 thrstate:
4 dd 0x5 ; LC_UNIXTHREAD
5 dd _thrstate ; size
6 dd 0x01 ; i386_THREAD_STATE
7 dd 0x10 ; i386_THREAD_STATE_COUNT
8 times 10 dd 0x00 ; cpu thread state
9 dd start ; eip
10 times 05 dd 0x00 ;
11 _thrstate equ $-thrstate
We can have as many dylib segments as dynamic libraries we would like to use. I’m only using libc since that’s where printf and exit live. I could have created stubs for dlopen, dlclose, dlsym and dlerror and used them to load libc and pull out printf and exit. Why bother, though, when the dynamic linker can do it for us?
1 ;;; Load command #9
2
3 dylib:
4 dd 0x0c ; LC_LOAD_DYLIB
5 dd _dylib ; size
6 dd 0x18 ; nameoff
7 dd 0x02 ; timestamp
8 dd 0x006F0103 ; currentver
9 dd 0x00010000 ; compatver
10 db '/usr/lib/libSystem.B.dylib', 0
11 align 4
12 _dylib equ $-dylib
It was a long road through the Mach-O header but we can finally relax and get some work done. There isn’t much to do apart from printing hello world and exiting but note the alignment of the stack on a 16-byte boundary, before each function call.
I’m taking the easy way out and aligning the stack one extra time, at the beginning of the program. This makes the rest of the alignment work much easier!
All values in the stack are 32-bit values. We are pushing a single argument which requires us to pad the stack with 12 more bytes (sub esp, 0x10). We pop arguments and padding right after the call to printf.
1 GLOBAL start
2
3 start:
4
5 and esp, 0xFFFFFFF0
6 sub esp, 0x10
7 mov dword [esp], hello.msg
8 call _printf
9 add esp, 0x10
10 mov eax, 0 ; set return code
11 call _exit
12 hlt
13
14 codesize equ $-start
Data and stubs are easy. Note the alignment to a page boundary. A jump to a 32-bit address takes 5 bytes, thus 5 halt instructions are used for each stub.
1 ;;; Data
2
3 align 4096
4
5 hello.msg db 'Hello, World!', 0x0a, 0x00
6
7 ;;; Stubs
8
9 align 4096
10
11 _printf:
12 times 5 hlt
13
14 _exit:
15 times 5 hlt
The symbol table has a well-defined format and each symbol needs to be described in excruciating detail!
1 ;;; Linkage
2
3 align 4096
4
5 symbols: ; symbol table
6
7 ; hello.msg
8
9 dd str01off ; nstrx
10 db 0x0e ; type
11 db 0x02 ; sect
12 dw 0x00 ; desc
13 dd hello.msg ; value
14
15 ; start
16
17 dd str02off ; nstrx
18 db 0x0f ; type
19 db 0x01 ; sect
20 dw 0x00 ; desc
21 dd start ; value
22
23 ; _printf
24
25 dd str03off ; nstrx
26 db 0x01 ; type N_EXT
27 db 0x00 ; sect
28 dw 0x0101 ; desc
29 dd _printf ; value
30
31 ; _exit
32
33 dd str04off ; nstrx
34 db 0x01 ; type N_EXT
35 db 0 ; sect
36 dw 0x0101 ; desc
37 dd _exit ; value
The indirect symbol table tells the dynamic linker that elements 2 and 3 of the symbol table need to be looked up and their stubs plugged.
1 indirect: ; indirect symbol table
2
3 dd 0x02 ; _printf
4 dd 0x03 ; _exit
The string table names the symbols above.
1 strings: ; string table
2
3 db 0x20, 0x00
4
5 str01 db 'hello.msg', 0x00
6 str02 db 'start', 0x00
7 str03 db '_printf', 0x00
8 str04 db '_exit', 0x00
9
10 str01off equ str01 - strings
11 str02off equ str02 - strings
12 str03off equ str03 - strings
13 str04off equ str04 - strings
14
15 _strings equ $-strings
16 _symbols equ $-symbols
I don’t expect you to generate Mac binaries by hand on Linux or Windows but I hope this tutorial will be of help if you ever decide to try!