Skip to main content

Memory

· 32 min read

Before we even step into the world of (virtual) memory, let's first learn of some tools which might come in handy when dealing with source programs (we'll describe them in lengths later):

  1. Object File Inspection. This can be done through various tools such as otool, objdump, and llvm-objdump.
  2. Debugger. Tools such as gdb and lldb can be helpful when inspecting how the program executes.
  3. Virtual Memory Information. Sometimes, we need to see how a process has arranged its virtual memory space. Tools such as vmmap, leaks, and pmap (also potentially strace) can be used for this purpose.

Other tools can come handy frequently as well. For instance, if we want to list the "files" currently owned by the process, lsof provides useful information.

Process Address Space Layout

A process is an instance of a program that is under execution. Some text use the term task to refer to a process. Historically, the Mach kernel defined some terms (excerpt from Mach (kernel)):

a "task" is a set of system resources that produce "threads" to run.
a "thread" is a single unit of execution, existing within the context of a task and shares the task's resources.

The linux kernel also uses the term task to create a process (struct task_struct defined in /include/linux/sched.h). The term task and process may be used interchangebly here. Recall the distinction between a process and a program. A program file may contain various sections; data and text segments being the most common ones. These sections are loaded into the memory by the Operating System's loader, where the sections are typically grouped together to form larger, contiguous blocks of memory called segments.

A programming guide is rather boring without first exploring the classic "Hello, World!" program:

/* hello.c */
#include <stdio.h>

int
main (void)
{
printf("Hello, World!\n");

return (0);
}

Compile the following file. I'm using Macbook Pro M1 and despite there being a gcc program, it's an alias of clang. Although not really needed, I've explicitly added the -O0 flag to indicate that the compiler not optimize the code:

$ clang -O0 -o hello hello.c

Before we check the binary file, I need to mention a quirk of macOS (probably does not include the x86_64 versions.) When you create a program that depends on the standard library, the compiler adds a reference to the standard library on your executable. This means that the program is not static, and during runtime, the library is loaded into the process's memory and function from the library is called. On Linux, the program ldd is used to check which libraries the executable depends on. gcc on Linux provides feature to create a completely static executable through the -static switch. Unfortunately, clang on macOS--despite having the -static switch--fails to create a static executable. The ld linker program raised an error stating that libcrt0.o was not found. This is the C runtime on macOS. I felt the need to provide this information because the object file we're inspecting contain "stubs"; a section used for dynamic linking. Let's view the object file through objdump:

$ objdump -S hello

hello: file format mach-o arm64

Disassembly of section __TEXT,__text:

0000000100003f68 <_main>:
100003f68: ff 83 00 d1 sub sp, sp, #32 ; subtract 32 from the stack pointer.
100003f6c: fd 7b 01 a9 stp x29, x30, [sp, #16] ; store pair of registers (x29 and x30) to [sp, #16]/(sp+16). [2]
100003f70: fd 43 00 91 add x29, sp, #16 ; add 16 to stack pointer and assign to x29.
100003f74: 08 00 80 52 mov w8, #0 ; move the value 0 to register w8.
100003f78: e8 0b 00 b9 str w8, [sp, #8] ; store value of register w8 to [sp, #8]/(sp+8).
100003f7c: bf c3 1f b8 stur wzr, [x29, #-4] ; store 32-bit zero value from wzr register to memory [x29, #-4]/(x29-4).
100003f80: 00 00 00 90 adrp x0, 0x100003000 <_main+0x18> ; calculate address of 4KB memory page and store in register x0.
100003f84: 00 a0 3e 91 add x0, x0, #4008 ; add 4008 to the content of x0 and store in x0.
100003f88: 05 00 00 94 bl 0x100003f9c <_printf+0x100003f9c> ; branch to instruction at memory address 0x100003f9c and save current instruction's address in x30/lr register. [1]
100003f8c: e0 0b 40 b9 ldr w0, [sp, #8] ; load 32-bit value into w0 from address [sp, #8]/(sp+8).
100003f90: fd 7b 41 a9 ldp x29, x30, [sp, #16] ; load pair of registers x29 and x30 from the memory address [sp, #16]/(sp+16).
100003f94: ff 83 00 91 add sp, sp, #32 ; add 32 to stack pointer and store in stack pointer.
100003f98: c0 03 5f d6 ret ; return from current subroutine (or function) to the address stored in Link Register (x29).

Disassembly of section __TEXT,__stubs:

0000000100003f9c <__stubs>:
100003f9c: 10 00 00 b0 adrp x16, 0x100004000 <__stubs+0x4> ; calculate address of 4KB memory page containing 0x100004000 (address within the __stubs section) and store in register x16.
100003fa0: 10 02 40 f9 ldr x16, [x16] ; load 64-bit value from memory address stored in register x16 and store the value in x16.
100003fa4: 00 02 1f d6 br x16 ; branch unconditionally to memory address stored in x16.

The -S flag instructs objdump (which is essentially llvm-objdump) to display source interleaved with the disassembly. This means that disassembly contains some information based on the source file (such as main section). The distinction is pretty clear in eBPF programs on Linux.
I've added comment on each instruction.
[1] You might have realized that there is no call instruction made to call the standard library function printf. On arm-based machines, the instruction bl (Branch Link) is apparently used.
For people new to ARM (like me), refer to Armv8-A Instruction Set Architecture.
[2] Working with a pair of register from a single instruction; stp and ldp, can be hard to follow. On the stp instruction above, what we're conveying is, "On (sp + 16), store the content of register x29, the first operand. On ((sp + 16) + 8), store the content of register x30." The notion is similar for ldp.
Not related to the instructions seen here, but apparently the first instruction of dyld start is: pacibsp. This is a new instruction in ARMv8.3-A, and it provides a feature called "pointer authentication." It's an acronym for "generate Pointer Authentication Code for Instruction address using key B and SP register." I won't discuss this further, but interested readers can refer to: The AArch64 processor (aka arm64), part 18: Return address protection.

Debugging hello.c

Before we start debugging, we need to understand how it presents registers and bytes.

  1. Using the reg[ister] read command displays the process's current register contents. In my machine, there are 34 general purpose registers. Each register holds 64 bits (8 bytes) of data. Register such as sp stores memory location.
  2. A memory location can be examined using the x command. One could also use me[mory] read <address>. To read the content of stack pointer, use me read $sp.
  3. A stack is typically 16-byte aligned. This is done for performance reasons as well as a requirement of x86_64 ABI calling convention. This is also the reason why you'd see stack addresses ending with a 0, like 0x16fdfeda0. Some people think that the alignment is 16-bytes (or 128-bits) because each cache line back in the days could hold 16-bytes of data. It's also for SSE instructions.
  4. You'll also notice some pattern regarding the libraries that are linked. Using the image list command displays the main executable and all dependent shared libraries. You'll notice that most of these libraries have something like this: 0x000000019fbae000, where the last three nibbles are zeroed out. This sugggests that they are 16*16*16=4096 bytes aligned. Recall that a page is typically 4KB.

Let's start some basic debugging now!

The lines beginning with ## indicates comment for the command below (I'm assuming we're at the directory where simple is located):

$ lldb
## create a target, could also use `target create <path/to/executable>`.
(lldb) file hello
Current executable set to '<path>/hello' (arm64).

## demo: create another target, which will be removed shortly.
(lldb) target create main
Current executable set to '<path>/main' (arm64).

## check for current target list, `*` signifies the current executable to work on.
(lldb) target list
Current targets:
target #0: <path>/hello ( arch=arm64-apple-macosx13.0.0, platform
=host )
* target #1: <path>/main ( arch=arm64-apple-macosx13.0.0, platform=h
ost )

## remove the currently set target (we no longer need `main` executable)
(lldb) target delete
1 targets deleted.

## we'll now focus on `hello`, set the breakpoint to the `main` function.
## There are a few idioms to achieve this, I'll show the verbose command.
(lldb) breakpoint set --name main
Breakpoint 1: where = hello`main, address = 0x0000000100003f68

## launch the current target.
(lldb) process launch
Process 38113 launched: '<path>/hello' (arm64)
Process 38113 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100003f68 hello`main
hello`main:
-> 0x100003f68 <+0>: sub sp, sp, #0x20
0x100003f6c <+4>: stp x29, x30, [sp, #0x10]
0x100003f70 <+8>: add x29, sp, #0x10
0x100003f74 <+12>: mov w8, #0x0
Target 0: (hello) stopped.

## Fetch the current register values.
(lldb) register read
General Purpose Registers:
x0 = 0x0000000000000001
x1 = 0x000000016fdff048
x2 = 0x000000016fdff058
x3 = 0x000000016fdff200
x4 = 0x0000000000000000
x5 = 0x0000000000000000
x6 = 0x0000000000000000
x7 = 0x0000000000000000
x8 = 0x000000010000d910
x9 = 0x0000000000000001
x10 = 0xa040a05e00000000
x11 = 0x0000000000000003
x12 = 0x0000000000000148
x13 = 0x0000000000004000
x14 = 0x0000000000000008
x15 = 0x0000000000000000
x16 = 0x0000000000000000
x17 = 0x0000000100003f68 hello`main
x18 = 0x0000000000000000
x19 = 0x0000000100003f68 hello`main
x20 = 0x000000010000c000
x21 = 0x000000010000d910
x22 = 0x000000016fdfeed0
x23 = 0x000000019fc2e396 "/usr/lib/dyld"
x24 = 0x000000016fdfee50
x25 = 0x0000000000000001
x26 = 0x0000000000000000
x27 = 0x0000000000000000
x28 = 0x0000000000000000
fp = 0x000000016fdff020
lr = 0x000000019fbb3f28 dyld`start + 2236
sp = 0x000000016fdfedc0
pc = 0x0000000100003f68 hello`main
cpsr = 0x80001000

## Read one byte from the address pointed by stack pointer.
(lldb) memory read -c1 $sp
0x16fdfedc0: 00 .

## Read 64 bytes starting from address pointed by stack pointer.
(lldb) me read -c64 $sp
0x16fdfedc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x16fdfedd0: 00 00 00 00 00 00 00 00 d8 5d 0a 00 01 00 00 00 .........]......
0x16fdfede0: 00 00 00 40 00 00 00 00 c0 20 01 00 01 00 00 00 ...@..... ......
0x16fdfedf0: b0 c0 09 00 01 00 00 00 00 c0 00 00 01 00 00 00 ................

## print out the byte pointed by `$sp + 24`, or (0x16fdfedd8)
(lldb) print *(char *)($sp+24)
(char) $14 = '\xd8'

## examine 24 units starting from address $sp.
## as we didn't specify the format specifier for unit (24). Possible ones are:
## `x/24x` -> hex `x/24i` -> instruction `x/24d` -> decimal
## `x/24s` -> string `x/24b` -> byte `x/24w` -> word
## `x/24g` -> giant word `x/24` -> default (word)
## it also seems to preserve the previously used format specifier.
## also, the output below shows (24 * 8) bytes of data starting from stack pointer.
(lldb) x/24 $sp
0x16fdfedc0: 0x0000000000000000 0x0000000000000000
0x16fdfedd0: 0x0000000000000000 0x00000001000a5dd8
0x16fdfede0: 0x0000000040000000 0x00000001000120c0
0x16fdfedf0: 0x000000010009c0b0 0x000000010000c000
0x16fdfee00: 0x00000001000a5dd8 0x0000000042000000
0x16fdfee10: 0x0000000100012078 0x000000010009c080
0x16fdfee20: 0x000000016fdfeed0 0x000000010000c000
0x16fdfee30: 0x000000010000c000 0x000000019fbae000
0x16fdfee40: 0xa040a05e00000000 0x0000000000000001
0x16fdfee50: 0x0000000000000000 0x0000000000000000
0x16fdfee60: 0x0000000000000000 0xffffffffffffffff
0x16fdfee70: 0x000000016fdf0000 0x000000010000c000

fp = 0x000000016fdff020
lr = 0x000000019fbb3f28 dyld`start + 2236

## examine 20 subsequent instructions (including $pc)
(lldb) x/20i $pc
-> 0x100003f68: 0xd10083ff sub sp, sp, #0x20
0x100003f6c: 0xa9017bfd stp x29, x30, [sp, #0x10]
0x100003f70: 0x910043fd add x29, sp, #0x10
0x100003f74: 0x52800008 mov w8, #0x0
0x100003f78: 0xb9000be8 str w8, [sp, #0x8]
0x100003f7c: 0xb81fc3bf stur wzr, [x29, #-0x4]
0x100003f80: 0x90000000 adrp x0, 0
0x100003f84: 0x913ea000 add x0, x0, #0xfa8 ; "Hello, World!\n"
0x100003f88: 0x94000005 bl 0x100003f9c ; symbol stub for: printf
0x100003f8c: 0xb9400be0 ldr w0, [sp, #0x8]
0x100003f90: 0xa9417bfd ldp x29, x30, [sp, #0x10]
0x100003f94: 0x910083ff add sp, sp, #0x20
0x100003f98: 0xd65f03c0 ret
0x100003f9c: 0xb0000010 adrp x16, 1
0x100003fa0: 0xf9400210 ldr x16, [x16]
0x100003fa4: 0xd61f0200 br x16
0x100003fa8: 0x6c6c6548 ldnp d8, d25, [x10, #-0x140]
0x100003fac: 0x57202c6f .long 0x57202c6f ; unknown opcode
0x100003fb0: 0x646c726f .long 0x646c726f ; unknown opcode
0x100003fb4: 0x00000a21 udf #0xa21

## list out the shared library as well as the main executable
(lldb) image list
[ 0] C7F3BA94-F9BC-31A0-ABB4-0A9E367290CB 0x0000000100000000 <path>/hello
[ 1] 2237410F-D39C-30CE-9A94-13AACB66B766 0x000000019fbae000 /usr/lib/dyld
[ 2] 56834D23-1BFF-310F-89FB-622D1F16A1BF 0x00000001ab896000 /usr/lib/libSystem.B.dylib
[ 3] FEA038BA-CC59-3085-93B0-AB8437AA6CE2 0x00000001ab890000 /usr/lib/system/libcache.dylib
[ 4] 34AC4B05-E145-3C58-8C24-1190770EAB31 0x00000001ab84c000 /usr/lib/system/libcommonCrypto.dylib
[ 5] 1D6552C4-49C4-374F-8371-198BCFC4174D 0x00000001ab877000 /usr/lib/system/libcompiler_rt.dylib
[ 6] E61C2838-9EA2-33CE-B96B-85FF38DB7744 0x00000001ab86d000 /usr/lib/system/libcopyfile.dylib
[ 7] 4A9F9101-A1B1-3FB7-89EA-746CFCE95099 0x000000019fca1000 /usr/lib/system/libcorecrypto.dylib
[ 8] C2FD3094-B465-39A4-B774-16583FF53C4B 0x000000019fd58000 /usr/lib/system/libdispatch.dylib
[ 9] A2947B47-B494-36D4-96C6-95977FFB51FB 0x000000019ff12000 /usr/lib/system/libdyld.dylib
[ 10] C4512BA5-7CA3-30AE-9793-5CC5417F0FC3 0x00000001ab886000 /usr/lib/system/libkeymgr.dylib
[ 11] 91A88FDF-FD27-32AF-A2CE-70F7E4065C3B 0x00000001ab826000 /usr/lib/system/libmacho.dylib
[ 12] A2D17FF6-CBC6-3D19-89E1-F5E57191E8A3 0x00000001aae2b000 /usr/lib/system/libquarantine.dylib
[ 13] 2213EE66-253B-3234-AA4D-B46F07C3540E 0x00000001ab883000 /usr/lib/system/libremovefile.dylib
[ 14] 68D76774-F8B4-36EA-AA35-0AB4044D56C7 0x00000001a4bbb000 /usr/lib/system/libsystem_asl.dylib
[ 15] 5541DF62-A795-3F57-A54C-1AEC4DD3E44C 0x000000019fc3d000 /usr/lib/system/libsystem_blocks.dylib
[ 16] 95A70E20-1DF3-3DDF-900C-315ED0B2C067 0x000000019fda3000 /usr/lib/system/libsystem_c.dylib
[ 17] BEB9DE52-6F49-370A-B45B-CBE6780E7083 0x00000001ab87b000 /usr/lib/system/libsystem_collections.dylib
[ 18] 121F8B4D-3939-300D-BE22-979D6B476361 0x00000001aa26a000 /usr/lib/system/libsystem_configuration.dylib
[ 19] 7CE9526A-B673-363A-8905-71D080974C0E 0x00000001a9300000 /usr/lib/system/libsystem_containermanager.dylib
[ 20] 54BF691A-0908-3548-95F2-34CFD58E5617 0x00000001ab51a000 /usr/lib/system/libsystem_coreservices.dylib
[ 21] 579733C7-851D-3B3E-83B5-FD203BA50D02 0x00000001a2d02000 /usr/lib/system/libsystem_darwin.dylib
[ 22] 4EFF0147-928F-3321-8268-655FE71DC209 0x00000001ab887000 /usr/lib/system/libsystem_dnssd.dylib
[ 23] 5068382F-DC0F-3824-8ED5-18A24B35FEF9 0x000000019fda0000 /usr/lib/system/libsystem_featureflags.dylib
[ 24] 4448FB99-7B1D-3E15-B7EE-3340FF0DA88D 0x000000019ff3e000 /usr/lib/system/libsystem_info.dylib
[ 25] 82E529F5-C4DF-3D42-9113-3A4F87FEF1A0 0x00000001ab7ee000 /usr/lib/system/libsystem_m.dylib
[ 26] 0AC99C6E-CB01-30E5-AB10-65AB990652A5 0x000000019fd2c000 /usr/lib/system/libsystem_malloc.dylib
[ 27] 3B2CC4A9-A5EE-3627-8293-4AF4D891074E 0x00000001a4b39000 /usr/lib/system/libsystem_networkextension.dylib
[ 28] E4AA6E5F-2501-3382-BFB3-64464E6D8254 0x00000001a316c000 /usr/lib/system/libsystem_notify.dylib
[ 29] 99FDEFF2-36F1-3436-B8B2-DE0003B5A4BF 0x00000001aa26f000 /usr/lib/system/libsystem_sandbox.dylib
[ 30] E529D1AC-D20A-3308-9033-E1712A9C655E 0x00000001ab880000 /usr/lib/system/libsystem_secinit.dylib
[ 31] 34A49B54-82B2-37A1-9314-F6A4A2BB3FF8 0x000000019fecb000 /usr/lib/system/libsystem_kernel.dylib
[ 32] F80C6971-C080-31F5-AB6E-BE01311154AF 0x000000019ff37000 /usr/lib/system/libsystem_platform.dylib
[ 33] 46D35233-A051-3F4F-BBA4-BA56DDDC4D1A 0x000000019ff05000 /usr/lib/system/libsystem_pthread.dylib
[ 34] F9F1F4BE-D97F-37A7-8382-552C22DF1BB4 0x00000001a6383000 /usr/lib/system/libsystem_symptoms.dylib
[ 35] 3F3E75B7-F0A7-30BB-9FD7-FD1307FE6055 0x000000019fc86000 /usr/lib/system/libsystem_trace.dylib
[ 36] E3BF7A76-2CBE-3DB9-8496-8BB6DBBE0CFC 0x00000001ab85a000 /usr/lib/system/libunwind.dylib
[ 37] F3F19227-FF8F-389C-A094-6F4C16E458AF 0x000000019fc42000 /usr/lib/system/libxpc.dylib
[ 38] 52AA13E2-567C-36C2-9494-7B892FDBF245 0x000000019feaf000 /usr/lib/libc++abi.dylib
[ 39] 5BEAFA2B-3AF4-3ED2-B054-1F58A7C851EF 0x000000019fb68000 /usr/lib/libobjc.A.dylib
[ 40] FB664621-26AE-3F46-8F5A-DD5D890A5CE7 0x00000001ab865000 /usr/lib/liboah.dylib
[ 41] 54E8FBE1-DF0D-33A2-B8FA-356565C12929 0x000000019fe22000 /usr/lib/libc++.1.dylib

(lldb) continue
Process 38113 resuming
Hello, World!
Process 38113 exited with status = 0 (0x00000000)

We've explored enough for a "simple" program. But we need not call a library function to properly debug and look at memory.

Poking around the unknown (dyld)

$ lldb
(lldb) target create hello
Current executable set to '<path>/hello' (arm64).

## I've found two way to stop a process at entry. I'll show one, but the other is:
## `process launch --stop-at-entry`
(lldb) breakpoint set --name start --shlib dyld
Breakpoint 1: where = dyld`start, address = 0x00000001800a766c

## Check the address where it is currently loaded. Note that we haven't started the program yet.
(lldb) image list
[ 0] 56B78BB7-AB3B-350E-918A-FFCDD60E670E 0x0000000100000000 <path>/hello
<path>/hello.dSYM/Contents/Resources/DWARF/hello
[ 1] 2237410F-D39C-30CE-9A94-13AACB66B766 0x00000001800a2000 /usr/lib/dyld
[ 2] 56834D23-1BFF-310F-89FB-622D1F16A1BF 0x000000018bd8a000 /usr/lib/libSystem.B.dylib
[ 3] FEA038BA-CC59-3085-93B0-AB8437AA6CE2 0x000000018bd84000 /usr/lib/system/libcache.dylib
[ 4] 34AC4B05-E145-3C58-8C24-1190770EAB31 0x000000018bd40000 /usr/lib/system/libcommonCrypto.dylib
[ 5] 1D6552C4-49C4-374F-8371-198BCFC4174D 0x000000018bd6b000 /usr/lib/system/libcompiler_rt.dylib
[ 6] E61C2838-9EA2-33CE-B96B-85FF38DB7744 0x000000018bd61000 /usr/lib/system/libcopyfile.dylib
[ 7] 4A9F9101-A1B1-3FB7-89EA-746CFCE95099 0x0000000180195000 /usr/lib/system/libcorecrypto.dylib
[ 8] C2FD3094-B465-39A4-B774-16583FF53C4B 0x000000018024c000 /usr/lib/system/libdispatch.dylib
[ 9] A2947B47-B494-36D4-96C6-95977FFB51FB 0x0000000180406000 /usr/lib/system/libdyld.dylib
[ 10] C4512BA5-7CA3-30AE-9793-5CC5417F0FC3 0x000000018bd7a000 /usr/lib/system/libkeymgr.dylib
[ 11] 91A88FDF-FD27-32AF-A2CE-70F7E4065C3B 0x000000018bd1a000 /usr/lib/system/libmacho.dylib
[ 12] A2D17FF6-CBC6-3D19-89E1-F5E57191E8A3 0x000000018b31f000 /usr/lib/system/libquarantine.dylib
[ 13] 2213EE66-253B-3234-AA4D-B46F07C3540E 0x000000018bd77000 /usr/lib/system/libremovefile.dylib
[ 14] 68D76774-F8B4-36EA-AA35-0AB4044D56C7 0x00000001850af000 /usr/lib/system/libsystem_asl.dylib
[ 15] 5541DF62-A795-3F57-A54C-1AEC4DD3E44C 0x0000000180131000 /usr/lib/system/libsystem_blocks.dylib
[ 16] 95A70E20-1DF3-3DDF-900C-315ED0B2C067 0x0000000180297000 /usr/lib/system/libsystem_c.dylib
[ 17] BEB9DE52-6F49-370A-B45B-CBE6780E7083 0x000000018bd6f000 /usr/lib/system/libsystem_collections.dylib
[ 18] 121F8B4D-3939-300D-BE22-979D6B476361 0x000000018a75e000 /usr/lib/system/libsystem_configuration.dylib
[ 19] 7CE9526A-B673-363A-8905-71D080974C0E 0x00000001897f4000 /usr/lib/system/libsystem_containermanager.dylib
[ 20] 54BF691A-0908-3548-95F2-34CFD58E5617 0x000000018ba0e000 /usr/lib/system/libsystem_coreservices.dylib
[ 21] 579733C7-851D-3B3E-83B5-FD203BA50D02 0x00000001831f6000 /usr/lib/system/libsystem_darwin.dylib
[ 22] 4EFF0147-928F-3321-8268-655FE71DC209 0x000000018bd7b000 /usr/lib/system/libsystem_dnssd.dylib
[ 23] 5068382F-DC0F-3824-8ED5-18A24B35FEF9 0x0000000180294000 /usr/lib/system/libsystem_featureflags.dylib
[ 24] 4448FB99-7B1D-3E15-B7EE-3340FF0DA88D 0x0000000180432000 /usr/lib/system/libsystem_info.dylib
[ 25] 82E529F5-C4DF-3D42-9113-3A4F87FEF1A0 0x000000018bce2000 /usr/lib/system/libsystem_m.dylib
[ 26] 0AC99C6E-CB01-30E5-AB10-65AB990652A5 0x0000000180220000 /usr/lib/system/libsystem_malloc.dylib
[ 27] 3B2CC4A9-A5EE-3627-8293-4AF4D891074E 0x000000018502d000 /usr/lib/system/libsystem_networkextension.dylib
[ 28] E4AA6E5F-2501-3382-BFB3-64464E6D8254 0x0000000183660000 /usr/lib/system/libsystem_notify.dylib
[ 29] 99FDEFF2-36F1-3436-B8B2-DE0003B5A4BF 0x000000018a763000 /usr/lib/system/libsystem_sandbox.dylib
[ 30] E529D1AC-D20A-3308-9033-E1712A9C655E 0x000000018bd74000 /usr/lib/system/libsystem_secinit.dylib
[ 31] 34A49B54-82B2-37A1-9314-F6A4A2BB3FF8 0x00000001803bf000 /usr/lib/system/libsystem_kernel.dylib
[ 32] F80C6971-C080-31F5-AB6E-BE01311154AF 0x000000018042b000 /usr/lib/system/libsystem_platform.dylib
[ 33] 46D35233-A051-3F4F-BBA4-BA56DDDC4D1A 0x00000001803f9000 /usr/lib/system/libsystem_pthread.dylib
[ 34] F9F1F4BE-D97F-37A7-8382-552C22DF1BB4 0x0000000186877000 /usr/lib/system/libsystem_symptoms.dylib
[ 35] 3F3E75B7-F0A7-30BB-9FD7-FD1307FE6055 0x000000018017a000 /usr/lib/system/libsystem_trace.dylib
[ 36] E3BF7A76-2CBE-3DB9-8496-8BB6DBBE0CFC 0x000000018bd4e000 /usr/lib/system/libunwind.dylib
[ 37] F3F19227-FF8F-389C-A094-6F4C16E458AF 0x0000000180136000 /usr/lib/system/libxpc.dylib
[ 38] 5BEAFA2B-3AF4-3ED2-B054-1F58A7C851EF 0x000000018005c000 /usr/lib/libobjc.A.dylib
[ 39] 52AA13E2-567C-36C2-9494-7B892FDBF245 0x00000001803a3000 /usr/lib/libc++abi.dylib
[ 40] FB664621-26AE-3F46-8F5A-DD5D890A5CE7 0x000000018bd59000 /usr/lib/liboah.dylib
[ 41] 54E8FBE1-DF0D-33A2-B8FA-356565C12929 0x0000000180316000 /usr/lib/libc++.1.dylib

## On another terminal, inspect the object file. Using `objdump` is not preferred on macOS.
## Although `objdump` will display the `start` section as well, but it'll show mutliple ones
## since `file /usr/lib/dyld` tells us that:
$ file /usr/lib/dyld
/usr/lib/dyld: Mach-O universal binary with 3 architectures: [i386:Mach-O dynamic linker i386] [x86_64:Mach-O 64-bit dynamic linker x86_64] [arm64e]
/usr/lib/dyld (for architecture i386): Mach-O dynamic linker i386
/usr/lib/dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
/usr/lib/dyld (for architecture arm64e): Mach-O 64-bit dynamic linker arm64e
$ objdump -D /usr/lib/dyld
...
start:
4d1c: 55 pushl %ebp
4d1d: 89 e5 movl %esp, %ebp
4d1f: 53 pushl %ebx
4d20: 57 pushl %edi
4d21: 56 pushl %esi
...
start:
5cb0: 55 pushq %rbp
5cb1: 48 89 e5 movq %rsp, %rbp
5cb4: 41 57 pushq %r15
5cb6: 41 56 pushq %r14
5cb8: 41 55 pushq %r13
...
start:
566c: 7f 23 03 d5 pacibsp
5670: fa 67 bb a9 stp x26, x25, [sp, #-80]!
5674: f8 5f 01 a9 stp x24, x23, [sp, #16]
5678: f6 57 02 a9 stp x22, x21, [sp, #32]
567c: f4 4f 03 a9 stp x20, x19, [sp, #48]
...
$ otool -tV /usr/lib/dyld
...
start:
000000000000566c pacibsp
0000000000005670 stp x26, x25, [sp, #-0x50]!
0000000000005674 stp x24, x23, [sp, #0x10]
0000000000005678 stp x22, x21, [sp, #0x20]
000000000000567c stp x20, x19, [sp, #0x30]
0000000000005680 stp x29, x30, [sp, #0x40]
0000000000005684 add x29, sp, #0x40
0000000000005688 sub sp, sp, #0x220
...

## Notice the offset of `start` routine. We know the base address of `dyld` image too, 0x00000001800a2000.
## (0x00000001800a2000 + 0x566c) = 0x1800A766C
## This is exactly where the breakpoint is set.
## Let's first check some instructions in that address:
(lldb) x/10i 0x00000001800a766c
0x1800a766c: 0xd503237f pacibsp
0x1800a7670: 0xa9bb67fa stp x26, x25, [sp, #-0x50]!
0x1800a7674: 0xa9015ff8 stp x24, x23, [sp, #0x10]
0x1800a7678: 0xa90257f6 stp x22, x21, [sp, #0x20]
0x1800a767c: 0xa9034ff4 stp x20, x19, [sp, #0x30]
0x1800a7680: 0xa9047bfd stp x29, x30, [sp, #0x40]
0x1800a7684: 0x910103fd add x29, sp, #0x40
0x1800a7688: 0xd10883ff sub sp, sp, #0x220
0x1800a768c: 0xaa0103f4 mov x20, x1
0x1800a7690: 0xaa0003f3 mov x19, x0

## The `image lookup` command will display two forms of output.
## Notice the address inside the square brackets is the absolute
## addressing. The ones inside the parentheses is the relative
## to a particular section, `dyld.__TEXT.__text` in this case.
(lldb) image lookup -s start -v
1 symbols match 'start' in /usr/lib/dyld:
Address: dyld[0x00000001800a766c] (dyld.__TEXT.__text + 18028)
Summary: dyld`start
Module: file = "/usr/lib/dyld", arch = "arm64e"
Symbol: id = {0x000000b2}, range = [0x00000001800a766c-0x00000001800a8064), name="start"

## Verify that the relative offset points to the same instruction as well. We'll first
## need to dump the image's section. Since we have multiple images loaded currently, we
## will only ask for `dyld`'s dump.
(lldb) image dump sections dyld
Sections for '/usr/lib/dyld' (arm64e):
SectID Type File Address Perm File Off. File Size Flags Section Name
---------- ---------------- --------------------------------------- ---- ---------- ---------- ---------- ----------------------------
0x00000100 container [0x00000001800a2000-0x0000000180130554) r-x 0x00000000 0x0008e554 0x00000000 dyld.__TEXT
0x00000001 code [0x00000001800a3000-0x0000000180120db0) r-x 0x00001000 0x0007ddb0 0x80000400 dyld.__TEXT.__text
0x00000002 regular [0x0000000180120db0-0x0000000180122308) r-x 0x0007edb0 0x00001558 0x00000000 dyld.__TEXT.__const
0x00000003 data-cstr [0x0000000180122308-0x000000018012fbed) r-x 0x00080308 0x0000d8e5 0x00000002 dyld.__TEXT.__cstring
0x00000004 regular [0x000000018012fbed-0x00000001801300d0) r-x 0x0008dbed 0x000004e3 0x00000000 dyld.__TEXT.__info_plist
0x00000005 compact-unwind [0x00000001801300d0-0x0000000180130554) r-x 0x0008e0d0 0x00000484 0x00000000 dyld.__TEXT.__unwind_info
0x00000200 container [0x00000001dd134c78-0x00000001dd13a350) rw- 0x5d092c78 0x000056d8 0x00000010 dyld.__DATA_CONST
0x00000006 regular [0x00000001dd134c78-0x00000001dd134cf8) rw- 0x5d092c78 0x00000080 0x00000000 dyld.__DATA_CONST.__auth_ptr
0x00000007 regular [0x00000001dd134cf8-0x00000001dd13a350) rw- 0x5d092cf8 0x00005658 0x00000000 dyld.__DATA_CONST.__const
0x00000300 container [0x00000001db8c5100-0x00000001db8c7ccc) rw- 0x5b823100 0x00002bcc 0x00000000 dyld.__DATA
0x00000008 data [0x00000001db8c5100-0x00000001db8c6eb8) rw- 0x5b823100 0x00001db8 0x00000000 dyld.__DATA.__data
0x00000009 zero-fill [0x00000001db8c6ec0-0x00000001db8c77b0) rw- 0x5b824ec0 0x000008f0 0x00000001 dyld.__DATA.__common
0x0000000a zero-fill [0x00000001db8c77b0-0x00000001db8c7ccc) rw- 0x5b8257b0 0x0000051c 0x00000001 dyld.__DATA.__bss
0x00000400 container [0x00000001db3d5bc0-0x00000001db3d76f4) rw- 0x5b333bc0 0x00001b34 0x00000000 dyld.__DATA_DIRTY
0x0000000b regular [0x00000001db3d5bc0-0x00000001db3d5d30) rw- 0x5b333bc0 0x00000170 0x00000000 dyld.__DATA_DIRTY.__all_image_info
0x0000000c regular [0x00000001db3d5d30-0x00000001db3d5d70) rw- 0x5b333d30 0x00000040 0x00000000 dyld.__DATA_DIRTY.__crash_info
0x0000000d data [0x00000001db3d5d70-0x00000001db3d5d84) rw- 0x5b333d70 0x00000014 0x00000000 dyld.__DATA_DIRTY.__data
0x0000000e zero-fill [0x00000001db3d5d88-0x00000001db3d5dc0) rw- 0x5b333d88 0x00000038 0x00000001 dyld.__DATA_DIRTY.__bss
0x0000000f zero-fill [0x00000001db3d5dc0-0x00000001db3d76f4) rw- 0x5b333dc0 0x00001934 0x00000001 dyld.__DATA_DIRTY.__common
0x00000500 container [0x000000022193c000-0x0000000253b18000) r-- 0xa189a000 0x321dc000 0x00000000 dyld.__LINKEDIT

## 18028 is the byte offset that was reported by the `image lookup` command previously.
## The hex representation of 18028 is 0x466c
## (0x00000001800a3000 + 0x466c) = 0x1800A766C
(lldb) x/10i '0x00000001800a3000 + 18028'
0x1800a766c: 0xd503237f pacibsp
0x1800a7670: 0xa9bb67fa stp x26, x25, [sp, #-0x50]!
0x1800a7674: 0xa9015ff8 stp x24, x23, [sp, #0x10]
0x1800a7678: 0xa90257f6 stp x22, x21, [sp, #0x20]
0x1800a767c: 0xa9034ff4 stp x20, x19, [sp, #0x30]
0x1800a7680: 0xa9047bfd stp x29, x30, [sp, #0x40]
0x1800a7684: 0x910103fd add x29, sp, #0x40
0x1800a7688: 0xd10883ff sub sp, sp, #0x220
0x1800a768c: 0xaa0103f4 mov x20, x1
0x1800a7690: 0xaa0003f3 mov x19, x0

Up until now, we've only inspected the target that will be executed shortly. Before we even launch/execute the program under the debugger, we can do various sorts of thing, as seen above. One thing needs to be emphasized. Despite the breakpoint existing in one address and being triggered in another (as can be verified from output below), we understand that before the program is launched, it is loaded into a different memory location and the breakpoint (the start symbol) is determined from the offset inside the image (dyld). Once we start the program, the kernel maps the images to the process's virtual memory. Although the address where the breakpoint was determine to trigger has been modified, the offset remains the same. When the process's current instruction pointer reaches the said offset, the breakpoint is successfully triggered.

## Run the program till it reaches the breakpoint.
(lldb) run
Process 66869 launched: '<path>/hello' (arm64)
Process 66869 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0x000000010001166c dyld`start
dyld`start:
-> 0x10001166c <+0>: pacibsp
0x100011670 <+4>: stp x26, x25, [sp, #-0x50]!
0x100011674 <+8>: stp x24, x23, [sp, #0x10]
0x100011678 <+12>: stp x22, x21, [sp, #0x20]
Target 0: (hello) stopped.

## Notice the address where it triggered the breakpoint seems different.
## You'll realize that images have been loaded in different addresses now.
(lldb) image list
[ 0] 56B78BB7-AB3B-350E-918A-FFCDD60E670E 0x0000000100000000 <path>/hello
<path>/hello.dSYM/Contents/Resources/DWARF/hello
[ 1] 2237410F-D39C-30CE-9A94-13AACB66B766 0x000000010000c000 /usr/lib/dyld

## Maybe it's a bug in lldb or something, but looking up `start` again will
## retrieve the old image's load address added with the offset. However,
## the range is accurate.
(lldb) image lookup -s start -v
1 symbols match 'start' in /usr/lib/dyld:
Address: dyld[0x00000001800a766c] (dyld.__TEXT.__text + 18028)
Summary: dyld`start
Module: file = "/usr/lib/dyld", arch = "arm64e"
Symbol: id = {0x000000b2}, range = [0x000000010001166c-0x0000000100012064), name="start"

## Currently, only two images will be loaded: the main executable `hello` and the dyld image.
## We'll again check the image list later when we set the breakpoint on `main` function
## and the program has been fully loaded. For now, let's check the sections of the images
## that are loaded for this process. Also, recall (base 10. 18028) = (base 16. 0x466c)
## (0x000000010000d000 + 0x466c) = 0x10001166C
(lldb) image dump sections
Dumping sections for 2 modules.
Sections for '<path>/hello' (arm64):
SectID Type Load Address Perm File Off. File Size Flags Section Name
---------- ---------------- --------------------------------------- ---- ---------- ---------- ---------- ----------------------------
0x00000100 container [0x0000000000000000-0x0000000100000000)* --- 0x00000000 0x00000000 0x00000000 hello.__PAGEZERO
0x00000200 container [0x0000000100000000-0x0000000100004000) r-x 0x00000000 0x00004000 0x00000000 hello.__TEXT
0x00000001 code [0x0000000100003f68-0x0000000100003f9c) r-x 0x00003f68 0x00000034 0x80000400 hello.__TEXT.__text
0x00000002 code [0x0000000100003f9c-0x0000000100003fa8) r-x 0x00003f9c 0x0000000c 0x80000408 hello.__TEXT.__stubs
0x00000003 data-cstr [0x0000000100003fa8-0x0000000100003fb7) r-x 0x00003fa8 0x0000000f 0x00000002 hello.__TEXT.__cstring
0x00000004 compact-unwind [0x0000000100003fb8-0x0000000100004000) r-x 0x00003fb8 0x00000048 0x00000000 hello.__TEXT.__unwind_info
0x00000300 container [0x0000000100004000-0x0000000100008000) rw- 0x00004000 0x00004000 0x00000010 hello.__DATA_CONST
0x00000005 data-ptrs [0x0000000100004000-0x0000000100004008) rw- 0x00004000 0x00000008 0x00000006 hello.__DATA_CONST.__got
0x00000400 container [0x0000000100008000-0x000000010000c000) r-- 0x00008000 0x000003d2 0x00000000 hello.__LINKEDIT
0x00000200 container [0x0000000100009000-0x000000010000a000)* rw- 0x00002000 0x000002e5 0x00000000 hello.__DWARF
0x00000001 dwarf-line [0x0000000100009000-0x0000000100009045)* rw- 0x00002000 0x00000045 0x00000000 hello.__DWARF.__debug_line
0x00000002 dwarf-aranges [0x0000000100009045-0x0000000100009075)* rw- 0x00002045 0x00000030 0x00000000 hello.__DWARF.__debug_aranges
0x00000003 dwarf-info [0x0000000100009075-0x00000001000090c8)* rw- 0x00002075 0x00000053 0x00000000 hello.__DWARF.__debug_info
0x00000004 dwarf-abbrev [0x00000001000090c8-0x0000000100009104)* rw- 0x000020c8 0x0000003c 0x00000000 hello.__DWARF.__debug_abbrev
0x00000005 dwarf-str [0x0000000100009104-0x0000000100009212)* rw- 0x00002104 0x0000010e 0x00000000 hello.__DWARF.__debug_str
0x00000006 apple-names [0x0000000100009212-0x000000010000924e)* rw- 0x00002212 0x0000003c 0x00000000 hello.__DWARF.__apple_names
0x00000007 apple-namespaces [0x000000010000924e-0x0000000100009272)* rw- 0x0000224e 0x00000024 0x00000000 hello.__DWARF.__apple_namespac
0x00000008 apple-types [0x0000000100009272-0x00000001000092c1)* rw- 0x00002272 0x0000004f 0x00000000 hello.__DWARF.__apple_types
0x00000009 apple-objc [0x00000001000092c1-0x00000001000092e5)* rw- 0x000022c1 0x00000024 0x00000000 hello.__DWARF.__apple_objc
Sections for '/usr/lib/dyld' (arm64e):
SectID Type Load Address Perm File Off. File Size Flags Section Name
---------- ---------------- --------------------------------------- ---- ---------- ---------- ---------- ----------------------------
0x00000100 container [0x000000010000c000-0x000000010009a554) r-x 0x00000000 0x0008e554 0x00000000 dyld.__TEXT
0x00000001 code [0x000000010000d000-0x000000010008adb0) r-x 0x00001000 0x0007ddb0 0x80000400 dyld.__TEXT.__text
0x00000002 regular [0x000000010008adb0-0x000000010008c308) r-x 0x0007edb0 0x00001558 0x00000000 dyld.__TEXT.__const
0x00000003 data-cstr [0x000000010008c308-0x0000000100099bed) r-x 0x00080308 0x0000d8e5 0x00000002 dyld.__TEXT.__cstring
0x00000004 regular [0x0000000100099bed-0x000000010009a0d0) r-x 0x0008dbed 0x000004e3 0x00000000 dyld.__TEXT.__info_plist
0x00000005 compact-unwind [0x000000010009a0d0-0x000000010009a554) r-x 0x0008e0d0 0x00000484 0x00000000 dyld.__TEXT.__unwind_info
0x00000200 container [0x000000010009c000-0x00000001000a16d8) rw- 0x5d092c78 0x000056d8 0x00000010 dyld.__DATA_CONST
0x00000006 regular [0x000000010009c000-0x000000010009c080) rw- 0x5d092c78 0x00000080 0x00000000 dyld.__DATA_CONST.__auth_ptr
0x00000007 regular [0x000000010009c080-0x00000001000a16d8) rw- 0x5d092cf8 0x00005658 0x00000000 dyld.__DATA_CONST.__const
0x00000300 container [0x00000001000a4000-0x00000001000a6bcc) rw- 0x5b823100 0x00002bcc 0x00000000 dyld.__DATA
0x00000008 data [0x00000001000a4000-0x00000001000a5db8) rw- 0x5b823100 0x00001db8 0x00000000 dyld.__DATA.__data
0x00000009 zero-fill [0x00000001000a5dc0-0x00000001000a66b0) rw- 0x5b824ec0 0x000008f0 0x00000001 dyld.__DATA.__common
0x0000000a zero-fill [0x00000001000a66b0-0x00000001000a6bcc) rw- 0x5b8257b0 0x0000051c 0x00000001 dyld.__DATA.__bss
0x00000400 container [0x00000001000a8000-0x00000001000a9b34) rw- 0x5b333bc0 0x00001b34 0x00000000 dyld.__DATA_DIRTY
0x0000000b regular [0x00000001000a8000-0x00000001000a8170) rw- 0x5b333bc0 0x00000170 0x00000000 dyld.__DATA_DIRTY.__all_image_info
0x0000000c regular [0x00000001000a8170-0x00000001000a81b0) rw- 0x5b333d30 0x00000040 0x00000000 dyld.__DATA_DIRTY.__crash_info
0x0000000d data [0x00000001000a81b0-0x00000001000a81c4) rw- 0x5b333d70 0x00000014 0x00000000 dyld.__DATA_DIRTY.__data
0x0000000e zero-fill [0x00000001000a81c8-0x00000001000a8200) rw- 0x5b333d88 0x00000038 0x00000001 dyld.__DATA_DIRTY.__bss
0x0000000f zero-fill [0x00000001000a8200-0x00000001000a9b34) rw- 0x5b333dc0 0x00001934 0x00000001 dyld.__DATA_DIRTY.__common
0x00000500 container [0x00000001000ac000-0x0000000132288000) r-- 0xa189a000 0x321dc000 0x00000000 dyld.__LINKEDIT

## Also, let's check the first 10 instructions from the current process counter:
(lldb) x/10i $pc
-> 0x10001166c: 0xd503237f pacibsp
0x100011670: 0xa9bb67fa stp x26, x25, [sp, #-0x50]!
0x100011674: 0xa9015ff8 stp x24, x23, [sp, #0x10]
0x100011678: 0xa90257f6 stp x22, x21, [sp, #0x20]
0x10001167c: 0xa9034ff4 stp x20, x19, [sp, #0x30]
0x100011680: 0xa9047bfd stp x29, x30, [sp, #0x40]
0x100011684: 0x910103fd add x29, sp, #0x40
0x100011688: 0xd10883ff sub sp, sp, #0x220
0x10001168c: 0xaa0103f4 mov x20, x1
0x100011690: 0xaa0003f3 mov x19, x0

Below, I'll describe some of the segments of the executable we saw above. Before we jump into it tho, I'd like to properly define the terminologies:

  1. Load Commands. A mach-o object begins with a fixed-size header. The header starts with a magic number (0xfeedface or 0xfeedfacf), then defines the object type, the target architecture, some flags and a number of load commands which follows. The load commands describe the rest of the object file contents. Using the -l switch on otool(1) displays the load commands for the respective file.
  2. Segments. A contiguous group of pages in virtual memory, which are mmap(2)-ed from the object file during the loading process. The file contents (including the header itself) are put into segments.
  3. Sections. Segments may further be divided into sections. A section is in effect a sub-mapping, taken from the same file mapping and in same address range. They commonly contain some distinct content (e.g., program code, strings, symbols, stubs, etc.)

When dumping the image's section, we can see numerous sections for a simple executable. I'll briefly describe some of them below (mostly excerpt from [JL_1]):

  1. __PAGEZERO. This segment is only found in executables. It was traditionally used to define the first page of virtual memory, but did not even map it--merely assigning the memory region no protections (---/---) instead. The idea behind doing so is to serve as a NULL pointer trap: This way, a NULL pointer dereference would fall in a region of memory disallowed by the MMU, triggering a bus fault (or segmentation fault). 64-bit binaries extend the range of __PAGEZERO to encompass all of the 32-bit address space. Doing so serves to prevent accidental mappings of 32-bit libraries into a 64-bit address space, as well trap the dereferencing of a 32-bit pointer. Refer to page 182 of [JL_1] to learn more about this segment.
  2. __TEXT. This segment contains program text, or code. On previous macOS versions, this segment could be both writeable and executable, but seems like newer ones disallow this. The text proper is located in __TEXT.__text section, but the symbol stubs helpers in __TEXT.__stubs and __TEXT.__stubhelper. It also stores read-only data, in additional sections such as __TEXT.__const and __TEXT.__cstring.
  3. __DATA. This segment is used for mutable data. It is marked rw-/rwx in MacOS, and rw-/rw- elsewhere. It also stores symbol pointers, which are used by dyld in the process of dynamically linking the binary.
  4. __DATA_CONST. This segment was introduced in Darwin 19. Given that the __DATA section is mutable, there is no guarantee that __DATA.__const is constant, nor are the various symbol pointer sections and Global Offset Table (GOT). This segment is mprotect(2)-ed by dyld to be read-only, and truly constant.
  5. __LINKEDIT. This segment serves as a generic container for file contents which are neither in __TEXT nor in __DATA. Unlike other segments, __LINKEDIT is not sectioned, and its contents are described by other load commands. These are commonly referred to as linkedit__data__commands, and contain no specific data of their own, instead specifying an offset into __LINKEDIT and its size.

NOTE:
Though rare, an additional reserved segment is __RESTRICT. This segment, when present, is an empty one, containing a single (also empty) section called __restrict. The mere presence of the section suffices for DYLD to ignore any environment variables when processing the binary, preventing tampering with the binary loading.
The DWARF segment is for debugging purposes, as the program was compiled using the -g switch and the debug symbols were made available.

The operations performed by the dynamic linker (dyld) is out of the scope for this article. I've tried looking into it a bit a most branch operations that occur within the dyld image contains the SuperVisor Call (svc) instruction and inspecting each of them would consume more time than I'm willing to make. Regardless, this section should provide some information on the role of dynamic linker when it comes to properly loading the process.

Disecting variable.c

The following source program is the one we'll be exploring next:

int   global_var = 0xbeef;
char bss_buffer[100];

int
main (void)
{

int local_var;
char local_buffer[10] = "examine";

local_var = global_var;

global_var++;

return (0);
}

Register Calling Convention and AArch64/x86_64 ABIs

An AArch64 architecture has the following attributes:

  1. There are 31 general purpose registers, labeled from x0 to x30. In contrast, x86_64 provides 16 general purpose registers: rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp, and register r8 to r15.

  2. On AArch64, register x0 to x7 are used for the first 8 integer or pointer arguments. On x86_64, registers: rdi, rsi, rdx, rcx, r8, and r9 are used for the first 6 integer or pointer arguments.

  3. On Aarch64, x0 is used for the return value of a function. x1 is used for second part of a 128-bit return value. On x86_64, rax is used for the return value of a function. rdx is used for the second part of a 128-bit return value.

  4. On AArch64, sp is a dedicated register (x31) that is used for stack operation. On x86_64, rsp is a dedicated register that points to the top of the stack.

  5. On AArch64, lr register (x30) holds the return address for a function call. x29 is used as the frame pointer. On x86_64, rbp is the frame pointer. The return address is pushed onto the stack by the call instruction. The ret instruction pops it off the stack and jumps to it.

  6. Before moving on to specific architecture, let's introduce a better term for what we'll be using here. A "caller-saved" register is identical to "call-clobbered" register, and a "callee-saved" register is identical to "call-preserved" register. A call-clobbered register is such which the called function can use the register and it's the caller's responsibility to store the register content (before the function was called) if it wants to preserve the value. In contrast, a call-preserved register is such who's content will be restored (to what it was before the function was called) before the called function returns.

    On AArch64, x0-x18 are call-clobbered register. On x86_64: rax, rcx, rdx, rdi, rsi, and r8-r11 are call-clobbered registers.

    On AArch64, x19-x28 are call-preserved register. On x86_64: rbx, rbp, r12-r15 are call-preserved registers.

  7. On AArch64, xzr register is a special purpose register that always reads as zero and discards any values written to it. There isn't a specific register on x86_64.

  8. The stack pointer must always be aligned to a 16-byte boundary before any function call.

  9. If a function takes more than 8 integer or 8 floating-point arguments, the additional arguments are passed onto the stack.

Disassembling variable.c

Despite being a trivial program, we can learn some things about where the different variables are located in the process's memory. We'll also take a look at some basic lldb command to move between stack frames. Lastly, we'll set up a watchpoint on the variable global_var such that the process stops whenever the variable global_var is modified.

$ lldb

(lldb) target create variable
Current executable set to '<path>/variable' (arm64).

## Let's disassemble the `main` function. We also tell
## lldb to source interleave the assembly using the `-m`
## switch. I'll try to comment out the instructions.
(lldb) dis -n main -m

5 main (void)
** 6 {

variable`main:
variable[0x100003f68] <+0>: sub sp, sp, #0x20 ; subtract 32 (0x20) from stack pointer and store the result [in sp].
variable[0x100003f6c] <+4>: mov w0, #0x0 ; move immediate value 0 to w0 register.
variable[0x100003f70] <+8>: str wzr, [sp, #0x1c] ; store 4-byte value 0 to mem address located in [sp + 28 (ox1c)].

1 int global_var = 0xbeef;
1 int global_var = 0xbeef;
2 char bss_buffer[100];

variable[0x100003f74] <+12>: adrp x8, 0 ; calculate base address of 4KB page containing program counter
; and store this address in x8. The second operand being 0
; tells to get the page where the instruction itself is located.
variable[0x100003f78] <+16>: add x8, x8, #0xfac ; add the immediate value 4012 (0xfac) to x8 and store the result in x8.

7
8 int local_var;
** 9 char local_buffer[10] = "examine";
10

variable[0x100003f7c] <+20>: ldr x9, [x8] ; load 8-byte value from memory address specified in x8 and store in x9.
variable[0x100003f80] <+24>: str x9, [sp, #0x8] ; store 8-byte value from x9 register to memory address locaed in
; [sp + 8 (0x8)]
variable[0x100003f84] <+28>: ldrh w8, [x8, #0x8] ; load half-word (16-bits) value from memory address in [x8 + 8 (0x8)]
; and zero fill remaining 16 bits (notice the `w` register prefix)
; and store the result.
variable[0x100003f88] <+32>: strh w8, [sp, #0x10] ; store half-word (lower 16-bits) value from register w8 to
; memory address referenced by [sp + 16 (0x10)]... [sp+0x10, sp+0x12)

1 int global_var = 0xbeef;

variable[0x100003f8c] <+36>: adrp x9, 1 ; calculate base address of the 4KB page that is 4096 bytes (4KB) after
; the page containing the current instruction and store this address
; in the 64-bit register x9.

** 11 local_var = global_var;
12

variable[0x100003f90] <+40>: ldr w8, [x9] ; load 32-bit value from memory address contained in register x9 and
; store it in register w8.
variable[0x100003f94] <+44>: str w8, [sp, #0x18] ; store 32-bit value contained in register w8 into the memory address
; at an offset of 28 (0x18) from sp, [sp + 28 (0x18)].

** 13 global_var++;
14

variable[0x100003f98] <+48>: ldr w8, [x9] ; load 32-bit value from memory address contained in x9 and store
; the value in register w8.
variable[0x100003f9c] <+52>: add w8, w8, #0x1 ; add 1 to the value in register w8 and store the result in w8.
variable[0x100003fa0] <+56>: str w8, [x9] ; store 32-bit value of register w8 to memory address contained in x9.

** 15 return (0);
16 }

variable[0x100003fa4] <+60>: add sp, sp, #0x20 ; add 32 (0x20) to the value in sp and store the result in sp.
variable[0x100003fa8] <+64>: ret ; load the return address from link register into the program counter.

Notes

  1. Darwin doesn't currently provide "fork notifications", so we can't implement [target.process.follow-fork-mode] feature on Darwin.

References:

[JL_1]: MacOS and iOS Internals, Volume I: User Mode (v1.3) [099105556X]