Sam*_*Sam 3 c macos arm64 apple-m1
I am playing around with longjmp and setjmp on my M1 MacBook Air. On an x86_64 Linux machine, setjmp populates a jmp_buf struct which has a long[] containing 'mangled' register values. Going through the glibc code, I was able to decode those values to get the stack pointer and frame pointer for instance.
On my M1 MackBook Air, this jmp_buf type seems to be an int[37] according to lldb. I can see the values, and print them but none of them match up to the stack pointer or frame pointer though some are close.
I am looking for how to decode macOS M1 jmp_buf array and get the stack pointer and frame pointer. Any source code would be welcome as well. So far, I've looked through glibc, specifically the sysdeps/aarch64 directory (x86_64 directory was what allowed me to decode on my linux machine), and this mirror of Apple's open source code. None of the jmp_buf structs match and I have been unable to determine whether mangling/munging is occurring.
I have:
#include <csetjmp>
#include <iostream>
int main() {
jmp_buf reg;
setjmp(reg);
int foo = 5;
std::cout << &foo << std::endl; // <--- Location on the stack, looking for something close to this
for (auto int offset = 0; offset < 37; offset++) {
std::cout << offset << ": "
<< (void*)reg[offset] // <--- Assumes registers are stored directly
<< ", "
<< (void*)reinterpret_cast<long*>(reg)[offset] // <--- Int array for some reason but registers are 64 bits, so maybe they're just next to each other?
<< std::endl;
}
return 0;
}
Run Code Online (Sandbox Code Playgroud)
Which prints something like:
% ./a.out
0x30c890358
0: 0xc6ac510, 0x10c6ac510
1: 0x1, 0x2918cc39c9b56814
2: 0xffffffffc9b56814, 0x2918cc39c9b56f44
3: 0x2918cc39, 0x10c6adc80
4: 0xffffffffc9b56f44, 0x30c890610
5: 0x2918cc39, 0x10c6adc60
6: 0xc6adc80, 0x1042221a0
7: 0x1, 0x2918cc3bc11e4ddb
8: 0xc890610, 0x1
9: 0x3, 0x37f00001f80
10: 0xc6adc60, 0x300000000
11: 0x1, 0x200000004
12: 0x42221a0, 0x10c6ac100
13: 0x1, 0x10c6ac100
14: 0xffffffffc11e4ddb, 0x100
15: 0x2918cc3b, 0x0
16: 0x1, 0x733d5f6c888800ad
17: 0x0, 0x10c6ac510
18: 0x1f80, 0x10c6adc60
19: 0x37f, 0x733d5f6c888800ad
20: 0x0, 0x30c8906a0
21: 0x3, 0x204580310
22: 0x4, 0x0
23: 0x2, 0x0
24: 0xc6ac100, 0x0
25: 0x1, 0x0
26: 0xc6ac100, 0x20461bde0
27: 0x1, 0x42000000
28: 0x100, 0x204580443
29: 0x0, 0x204612010
30: 0x0, 0x30c890490
31: 0x0, 0x20457a000
32: 0xffffffff888800ad, 0x20457a000
33: 0x733d5f6c, 0x20461bde0
34: 0xc6ac510, 0x40000000
35: 0x1, 0x20458049d
36: 0xc6adc60, 0x204612040
Run Code Online (Sandbox Code Playgroud)
I am expecting there to be a value in the jmp_buf array that is only a few bytes away from the address of foo. Offset #4 when interpreted as an array of long gets close but is farther than I would expect.
I am looking for the offset definitions and any demangling of the values that needs to happen.
Oh this is devious on so many levels.
For a start: your code is running in Rosetta.
My educated guess is that you're running either an IDE (VS Code?) or a terminal emulator that's x86_64, from which you're invoking the compiler, which will then also run as x86_64, with no explicit target arch flag, which will make it default to x86_64. Use -arch arm64 to cc/c++/gcc/g++/clang/clang++ to target arm64 explicitly, or prefix the compiler invocation with arch -arm64 [...] to run the entire process hierarchy natively.
Now, how did I determine that your code was running under Rosetta? It's what Apple calls "pointer munging". So the official Apple source dumps happen on github.com/apple-oss-distributions, and setjmp and longjmp are implemented in src/setjmp in libplatform, with hand-rolled assembly implementations for each architecture. The arm64 implementation is this:
ENTRY_POINT(__longjmp)
ldp x19, x20, [x0, JMP_r19_20]
ldp x21, x22, [x0, JMP_r21_22]
ldp x23, x24, [x0, JMP_r23_24]
ldp x25, x26, [x0, JMP_r25_26]
ldp x27, x28, [x0, JMP_r27_28]
ldp x10, x11, [x0, JMP_fp_lr]
ldr x12, [x0, JMP_sp_rsvd]
ldp d8, d9, [x0, JMP_d8_d9]
ldp d10, d11, [x0, JMP_d10_d11]
ldp d12, d13, [x0, JMP_d12_d13]
ldp d14, d15, [x0, JMP_d14_d15]
_OS_PTR_MUNGE_TOKEN(x16, x16)
_OS_PTR_UNMUNGE(fp, x10, x16)
_OS_PTR_UNMUNGE(lr, x11, x16)
_OS_PTR_UNMUNGE(x12, x12, x16)
ldrb w16, [sp] /* probe to detect absolutely corrupt stack pointers */
mov sp, x12
cmp w1, #0
csinc w0, w1, wzr, ne
ret
Run Code Online (Sandbox Code Playgroud)
It makes quite heavy use of macros, so here's the raw disassembly of _longjmp from /usr/lib/system/libsystem_platform.dylib:
;-- __longjmp:
0x00001d68 135040a9 ldp x19, x20, [x0]
0x00001d6c 155841a9 ldp x21, x22, [x0, 0x10]
0x00001d70 176042a9 ldp x23, x24, [x0, 0x20]
0x00001d74 196843a9 ldp x25, x26, [x0, 0x30]
0x00001d78 1b7044a9 ldp x27, x28, [x0, 0x40]
0x00001d7c 0a2c45a9 ldp x10, x11, [x0, 0x50]
0x00001d80 0c3040f9 ldr x12, [x0, 0x60]
0x00001d84 0824476d ldp d8, d9, [x0, 0x70]
0x00001d88 0a2c486d ldp d10, d11, [x0, 0x80]
0x00001d8c 0c34496d ldp d12, d13, [x0, 0x90]
0x00001d90 0e3c4a6d ldp d14, d15, [x0, 0xa0]
0x00001d94 70d03bd5 mrs x16, tpidrro_el0
0x00001d98 101e40f9 ldr x16, [x16, 0x38]
0x00001d9c 5d0110ca eor x29, x10, x16
0x00001da0 7e0110ca eor x30, x11, x16
0x00001da4 8c0110ca eor x12, x12, x16
0x00001da8 f0034039 ldrb w16, [sp]
0x00001dac 9f010091 mov sp, x12
0x00001db0 3f000071 cmp w1, 0
0x00001db4 20149f1a csinc w0, w1, wzr, ne
0x00001db8 c0035fd6 ret
Run Code Online (Sandbox Code Playgroud)
So the registers fp, lr and sp are stored at offsets 0x50, 0x58 and 0x60, but they're also XORed with a value loaded from [tpidrro_el0, 0x38]. The definitions of those MUNGE macros can be found in xnu/libsyscall/os/tsd.h, but they really don't tell you more than [tpidrro_el0, 0x38] either. It's just a per-process cookie that's XORed into those values. Which looks like this, if your code runs on arm64:
0x16b9bb0f0
0: 0x4446fbc, 0x104446fbc
1: 0x1, 0x104450000
2: 0x4450000, 0x104451910
3: 0x1, 0x16b9bb2e0
4: 0x4451910, 0x1a91ea396
5: 0x1, 0x16b9bb260
6: 0x6b9bb2e0, 0x1
7: 0x1, 0x0
8: 0xffffffffa91ea396, 0x0
9: 0x1, 0x0
10: 0x6b9bb260, 0x6f5a9a069b197d28
11: 0x1, 0x2a649a06f4c6a30c
12: 0x1, 0x6f5a9a069b197c08
13: 0x0, 0x0
14: 0x0, 0x0
15: 0x0, 0x0
16: 0x0, 0x0
17: 0x0, 0x0
18: 0x0, 0x0
19: 0x0, 0x0
20: 0xffffffff9b197d28, 0x0
21: 0x6f5a9a06, 0x0
22: 0xfffffffff4c6a30c, 0x100000000
23: 0x2a649a06, 0x104450000
24: 0xffffffff9b197c08, 0x31232f62314200ab
25: 0x6f5a9a06, 0x16b9bb430
26: 0x0, 0x1a916ff28
27: 0x0, 0x0
28: 0x0, 0x0
29: 0x0, 0x0
30: 0x0, 0x1045dddd8
31: 0x0, 0x40000000
32: 0x0, 0x10454a0c0
33: 0x0, 0x1045d40b0
34: 0x0, 0x104544000
35: 0x0, 0x1045dddd8
36: 0x0, 0x42000000
Run Code Online (Sandbox Code Playgroud)
Notice how the high bits at offsets 10 and 12 are identical? That's because the high bits in these registers are normally zero in userland, so if you XOR a 64-bit constant into them, the high bits will be the same. That is not at all what I see in your jmp_buf dump. Your values at these indices look more like bitmasks. Where I do see this, however, are indices 1 and 2. Which is precisely how the x86_64 implementation works:
;-- __longjmp:
0x00003d2c dbe3 fninit
0x00003d2e 85f6 test esi, esi
0x00003d30 b801000000 mov eax, 1
0x00003d35 0f45c6 cmovne eax, esi
0x00003d38 488b1f mov rbx, qword [rdi]
0x00003d3b 488b7708 mov rsi, qword [rdi + 8]
0x00003d3f 654833342538. xor rsi, qword gs:[0x38]
0x00003d48 4889f5 mov rbp, rsi
0x00003d4b 488b7710 mov rsi, qword [rdi + 0x10]
0x00003d4f 654833342538. xor rsi, qword gs:[0x38]
0x00003d58 4c0fbe26 movsx r12, byte [rsi]
0x00003d5c 4889f4 mov rsp, rsi
0x00003d5f 4c8b6718 mov r12, qword [rdi + 0x18]
0x00003d63 4c8b6f20 mov r13, qword [rdi + 0x20]
0x00003d67 4c8b7728 mov r14, qword [rdi + 0x28]
0x00003d6b 4c8b7f30 mov r15, qword [rdi + 0x30]
0x00003d6f 488b7738 mov rsi, qword [rdi + 0x38]
0x00003d73 654833342538. xor rsi, qword gs:[0x38]
0x00003d7c d96f4c fldcw word [rdi + 0x4c]
0x00003d7f 0fae5748 ldmxcsr dword [rdi + 0x48]
0x00003d83 fc cld
0x00003d84 ffe6 jmp rsi
Run Code Online (Sandbox Code Playgroud)
So yeah, that's how I knew. But back to my arm64 dump above, if we look at the assembly, then we would expect not just indices 10 and 12 to have the same high bits, but also index 11 (lr), but that's not the case. So what's going on there?
Well, it turns out we're not really running the arm64 version either. We're running arm64e! In case that doesn't mean anything to you, it's a separate Apple ABI with support for ARMv8.3 Pointer Authentication. Mach-O loaders will always prefer arm64e slices over arm64 if the hardware supports it, and since all Apple Silicon Macs do so, and since all stock binaries ship with an arm64e slice, libsystem_platform.dylib will always have its arm64e slice loaded (unless you manage to manually mess with it enough, maybe?). Either way, here's the real implementations of _setjmp and _longjmp that are actually running:
;-- __setjmp:
0x00001a54 7f2303d5 pacibsp
0x00001a58 ea031daa mov x10, x29
0x00001a5c ea0fc1da pacdb x10, sp
0x00001a60 ec030091 mov x12, sp
0x00001a64 a97d9952 mov w9, 0xcbed
0x00001a68 2c0dc1da pacdb x12, x9
0x00001a6c 70d03bd5 mrs x16, tpidrro_el0
0x00001a70 101e40f9 ldr x16, [x16, 0x38]
0x00001a74 4a0110ca eor x10, x10, x16
0x00001a78 cb0310ca eor x11, x30, x16
0x00001a7c 8c0110ca eor x12, x12, x16
0x00001a80 135000a9 stp x19, x20, [x0]
0x00001a84 155801a9 stp x21, x22, [x0, 0x10]
0x00001a88 176002a9 stp x23, x24, [x0, 0x20]
0x00001a8c 196803a9 stp x25, x26, [x0, 0x30]
0x00001a90 1b7004a9 stp x27, x28, [x0, 0x40]
0x00001a94 0a2c05a9 stp x10, x11, [x0, 0x50]
0x00001a98 0c3000f9 str x12, [x0, 0x60]
0x00001a9c 0824076d stp d8, d9, [x0, 0x70]
0x00001aa0 0a2c086d stp d10, d11, [x0, 0x80]
0x00001aa4 0c34096d stp d12, d13, [x0, 0x90]
0x00001aa8 0e3c0a6d stp d14, d15, [x0, 0xa0]
0x00001aac 00008052 mov w0, 0
0x00001ab0 ff0f5fd6 retab
;-- __longjmp:
0x00001ab4 135040a9 ldp x19, x20, [x0]
0x00001ab8 155841a9 ldp x21, x22, [x0, 0x10]
0x00001abc 176042a9 ldp x23, x24, [x0, 0x20]
0x00001ac0 196843a9 ldp x25, x26, [x0, 0x30]
0x00001ac4 1b7044a9 ldp x27, x28, [x0, 0x40]
0x00001ac8 0a2c45a9 ldp x10, x11, [x0, 0x50]
0x00001acc 0c3040f9 ldr x12, [x0, 0x60]
0x00001ad0 0824476d ldp d8, d9, [x0, 0x70]
0x00001ad4 0a2c486d ldp d10, d11, [x0, 0x80]
0x00001ad8 0c34496d ldp d12, d13, [x0, 0x90]
0x00001adc 0e3c4a6d ldp d14, d15, [x0, 0xa0]
0x00001ae0 70d03bd5 mrs x16, tpidrro_el0
0x00001ae4 101e40f9 ldr x16, [x16, 0x38]
0x00001ae8 4a0110ca eor x10, x10, x16
0x00001aec 7e0110ca eor x30, x11, x16
0x00001af0 8c0110ca eor x12, x12, x16
0x00001af4 a97d9952 mov w9, 0xcbed
0x00001af8 2c1dc1da autdb x12, x9
0x00001afc 9f0140f9 ldr xzr, [x12]
0x00001b00 9f010091 mov sp, x12
0x00001b04 ea1fc1da autdb x10, sp
0x00001b08 fd030aaa mov x29, x10
0x00001b0c 3f000071 cmp w1, 0
0x00001b10 20149f1a csinc w0, w1, wzr, ne
0x00001b14 ff0f5fd6 retab
Run Code Online (Sandbox Code Playgroud)
This does not seem to be open source at all at this point. And if I had to guess, it's probably also not stable ABI. The entire arm64e sub-architecture is considered not stable, subject to change without notice, and on macOS requires the -arm64e_preview_abi kernel boot-arg (which in turn requires downgraded OS security) in order for you to even be allowed to run non-Apple-signed arm64e binaries. So yeah, just don't rely on this code staying the same.
But alright, lr is different due to pointer authentication. setjmp does a pacibsp, and then longjmp does the corresponding retab. So far so good, except fp and sp also have their pacdb and autdb, which should add pointer authentication just the same. But here's another little detail: if you're running an arm64 binary on arm64e capable-hardware, then you will have arm64e libraries loaded in your process, but you're still running as just arm64. The pointer authentication instructions are turned off for your process via hardware flags in SCTLR_EL1, except for the IB keys. So the pacib* family of instructions will work, the pacia*, pacda* and pacdb* ones will not. But when reading the fp/lr/sp values yourself, basically you'll want to strip the pointer authentication bits, and let the hardware decide whether that's a nop or not.
So here's some code that prints these three registers on arm64, on macOS 13.4.1, no guarantees on forwards or backwards compatibility:
;-- __longjmp:
0x00001d68 135040a9 ldp x19, x20, [x0]
0x00001d6c 155841a9 ldp x21, x22, [x0, 0x10]
0x00001d70 176042a9 ldp x23, x24, [x0, 0x20]
0x00001d74 196843a9 ldp x25, x26, [x0, 0x30]
0x00001d78 1b7044a9 ldp x27, x28, [x0, 0x40]
0x00001d7c 0a2c45a9 ldp x10, x11, [x0, 0x50]
0x00001d80 0c3040f9 ldr x12, [x0, 0x60]
0x00001d84 0824476d ldp d8, d9, [x0, 0x70]
0x00001d88 0a2c486d ldp d10, d11, [x0, 0x80]
0x00001d8c 0c34496d ldp d12, d13, [x0, 0x90]
0x00001d90 0e3c4a6d ldp d14, d15, [x0, 0xa0]
0x00001d94 70d03bd5 mrs x16, tpidrro_el0
0x00001d98 101e40f9 ldr x16, [x16, 0x38]
0x00001d9c 5d0110ca eor x29, x10, x16
0x00001da0 7e0110ca eor x30, x11, x16
0x00001da4 8c0110ca eor x12, x12, x16
0x00001da8 f0034039 ldrb w16, [sp]
0x00001dac 9f010091 mov sp, x12
0x00001db0 3f000071 cmp w1, 0
0x00001db4 20149f1a csinc w0, w1, wzr, ne
0x00001db8 c0035fd6 ret
Run Code Online (Sandbox Code Playgroud)
Note 1: normally the instructions xpaci and xpacd would not be accepted by the assembler if targeting arm64, but with the .arch v8.3a directive, we can convince it to let us pass.
Note 2: this code will only work on ARMv8.3 hardware, due to the xpaci and xpacd instructions. If your code might run on hardware that doesn't support PAC (such as iOS devices before A12), then you'll need to change those. For xpaci it's possible to construct a backwards-compatible variant with xpaclri, which is encoded in historical NOP instruction space (see another answer of mine), but for xpacd there is no equivalent in NOP space, so what you'd have to do there is first determine what kind of hardware you're running on (via things like xpaclri), and then conditionally call into pointer authentication gadgets. But that's out of scope for this answer. :)