When i learn mach-o format before,i just know a little about stub. Now i have enough free time to study it. so record it. I try to explain how stubs work detaily,hope it can helpful.

Tool

Workflow

prepare

  1. Use Xcodestart a Command Line Toolproject in macOS, here i just named stubDebugfor demonstrate. Then replace the default NSLog(@"Hello world");to printf("Hello, World!\n");,like below:

    1
    2
    3
    4
    5
    6
    7
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // insert code here...
    printf("Hello, World!\n");
    }
    return 0;
    }
  1. Compile the project to generate the executable stubDebug(mach-o format),the drag it to both MachOViewand Hopper Disassember

analysis

In Hopper Disassember,let’s start with _mainlabel,we can find an instruction below

1
0000000100000f48         call       imp___stubs__printf

this is the point where our source codeprintf("Hello, World!\n");execute.

Click the imp___stubs__printflabel ,jump to

1
0000000100000f6e         jmp        qword [_printf_ptr]

Then click _printf_prtlabel, jump to

1
0000000100001020         dq         _printf

The _printfis just a tip,not a acture address.So we will find the address 0x100001020 in MachOView,it locates in __DATA,_la_symbol_ptr

1
100001020  0000000100000F98 Indirect Pointer [0x100001020 -> _print]

Then go to address 0x100000F98,it locates in __TXEXT,__stub_helper

For easy, we use Hopper Disassemberto ayalysis go on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     ; Section __stub_helper
0000000100000f74 lea r11, qword [dyld_stub_binder_100001000+8] ; CODE XREF=0x100000f89, 0x100000f93, 0x100000f9d
0000000100000f7b push r11
0000000100000f7d jmp qword [dyld_stub_binder_100001000] ; dyld_stub_binder
0000000100000f83 db 0x90
0000000100000f84 push 0x0
0000000100000f89 jmp 0x100000f74
0000000100000f8e push 0x1f
0000000100000f93 jmp 0x100000f74
0000000100000f98 push 0x3f ;
0000000100000f9d jmp 0x100000f74

dyld_stub_binder_100001000:
0000000100001000 dq dyld_stub_binder ; DATA XREF=0x100000f7d
0000000100001008 dq 0x0000000000000000 ; DATA XREF=0x100000f74

When we arrive in address 0x100000f98, then we push 0x3f, push 0x100001008,then jump dyld_stub_binder.

What do 0x3fand 0x100001008mean? We will explain it for later, for now, we just consider they are two numberes.

Then wo search dyld_sub_binderin dyld-551.3source code. It’t a assembler code.We look at the __x86_64__architecture,

What a bad luck, it’s too long! Don’t lose heart.We will only analysis some instruction below:

1
2
3
4
 movq		MH_PARAM_RBP(%rbp),%rdi	# call fastBindLazySymbol(loadercache, lazyinfo)
movq LP_PARAM_RBP(%rbp),%rsi
call __Z21_dyld_fast_stub_entryPvl
movq %rax,%r11 # copy jump target

Then we search fastBindLazySymbol,it’s a c++code.The 0x3fand 0x100001008are two parameters here actually (0x3f -> lazyBindingInfoOffset, 0x100001008 -> imageLoaderCache)

Now,imageLoaderCacheis 0x100001008, *imageLoaderCacheis the data located in address 0x100001008,we can find the the data in address 0x100001008 is 0x0000000 in MachOView.

So we will arrive at dyld::findMappedRange,it’t a fast address->image lookups. Simply explain, it will find the ImageLoader* where address 0x100001008locates in. It’s our stubDebugmain executable certainly.

Then execute doBindFastLazySymbol function. Let’s look at the ImageLoaderMachOCompressed::doBindFastLazySymbol

First will focus on code below:

1
getLazyBindingInfo(lazyBindingInfoOffset, start, end, &segIndex, &segOffset, &libraryOrdinal, &symbolName, &doneAfterBind)

The code will analysis Laze Binding Infofor offset 0x3f.Open MachOView again, find Dynamic Loader Info ->Lazy Binding Info.The Lazy Binding Info start as 0x100002020, then add offset 0x3f,we get address 0x10000205f,

1
2
3
4
5
6
7
10000205E 00                BIND_OPCODE_DONE
10000205F 72 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB segment(2)
100002060 20 uleb128 offset(32)
100002061 13 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM dylib(3)
100002062 40 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM falgs(0)
100002063 5F7072696E746600 string name(_printf)
10000206B 90 BIND_OPCODE_DO_BIND

The MachOViewis already help us to explain the meaning for every filed. So we go back ImageLoaderMachOCompressed::doBindFastLazySymbol , know that:

1
2
3
4
segIndex = 2;    // 0 is `__PAGEZERO`,1 is `__TEXT`,2 is `__DATA`
segOffset = 32;
libraryOrdinal = 3; // 1:LC_LOAD_DYLIB(Foundation) 2:LC_LOAD_DYLIC(libobjc.A.dylib) 3:LC_LOAD_DYLIB(libSystem.B.dylib)
symbolName = "_printf";

The general idea for this Lazy Binding Infois go to dylib(3)find symbol _printfaddress , then fill the address in this segment(2) segment with offset(32).

Then focus on code below:

1
2
  uintptr_t address = segActualLoadAddress(segIndex) + segOffset;
result = this->bindAt(context, address, BIND_TYPE_POINTER, symbolName, 0, 0, libraryOrdinal, "lazy ", NULL, true);

segIndex =2,is’s __DATAsegment.(0 is __PAGEZERO,1 is __TEXT,2 is __DATA).

Follow the segActualLoadAddressfunction,look at the LC_SEGMENT_64(__DATA)in MachOView,The VM Address is 0x100001000,then add offset 0x20(segOffset=32),we get address = 0x100001020; It is the _printfplaceholer. We know it before actually ,the 0x100001020will jump __stub_helperbefore ,but now we will fill in actual address of _printf.

1
100001020  0000000100000F98 Indirect Pointer [0x100001020 -> _print]

Then look at bindAtfunction,

1
2
3
4
5
 // resolve symbol
symbolAddress = this->resolve(context, symbolName, symbolFlags, libraryOrdinal, &targetImage, last, runResolver);

// do actual update
return this->bindLocation(context, addr, symbolAddress, type, symbolName, addend, this->getPath(), targetImage ? targetImage->getPath() : NULL, msg);

The resolve call stack is almost follows:

1
2
3
4
5
6
7
-resolve()
-libImage((unsigned int)libraryOrdinal-1)
-resolveTwolevel()
-findExportedSymbolAddress()
-findExportedSymbol()
-ImageLoaderMachOCompressed::findShallowExportedSymbol()
-getExportedSymbolAddress()

Let’s focus on libImage((unsigned int)libraryOrdinal-1)first. the libraryOrdinalis 3 actually depend on above.In this stubDebugproject,libImage(3-1) mean libSystem.B.dylib.Why? We can see stubDebug in MachOView for Load Commandspart. We can see

1
2
3
LC_LOAD_DYLIB(Foundation)
LC_LOAD_DYLIB(libobjc.A.dylib)
LC_LOAD_DYLIB(libSystem.B.dylib)

Then focus on findExportedSymbol, Because libSystem.B.dylibis a collection of libsystem_c.dylib,libsystem_kernal.dylib…… It will look up for ecah in recursive way.

We know _printfis in libsystem_c.dylib,,so we assume we are in libsystem_c.dylib,then execute findShallowExportedSymbol,this function look up Dynamic Loader Info ->Export Info of libsystem_c.dylib

Open libsystem_c.dylibinMachOView,The Export Infois a trie,we can find _printf in logic below:

1
2
3
4
5
6
7
92972 5F00 Node Lable '_'
92974 25 Next Node 0x92980
929E9 7000 Node Label 'p'
929EB 8F3C Next Node 0x94777
94700 72696E746600 Node Label 'rintf'
9779F B08401 Next Node 0x96B98
96B9A C49D10 Symbol Offset Ox40EC4

Now, we get the symbol offset 0x40EC4,this is the address of the symbol _printfactually.We can confirm it in Hopper Disassembler

1
2
                     _printf:
0000000000040ec4 push rbp

Now,We find the address of symbol _printf, and we also know we should bind it to stubDebug‘s address 0x100001020in __DATA,_la_symbol_ptr,to replace __TEXT,__stub_helperwith_printf,We finish the lazy bind.

Finally,we go back dylb_stub_binder

1
2
3
4
5
6
7
8
9
10
Lbind:
movq MH_PARAM_RBP(%rbp),%rdi # call fastBindLazySymbol(loadercache, lazyinfo)
movq LP_PARAM_RBP(%rbp),%rsi
call __Z21_dyld_fast_stub_entryPvl
movq %rax,%r11 # copy jump target
......
Ldone:
......
addq $16,%rsp # remove meta-parameters
jmp *%r11 # jmp to target

We jump _printf to finish the statement printf("Hello World!\n"). When call _printfnext time,we will call directly ,rather than by _stub_helper

Summary

  1. Generally called symbol is const char * structure.
  2. Store export symbol info is trie, trie can reduce memory.
  3. The lazy bind process is very likely cache mechanism.