Я играю с longjmp и setjmp на своем M1 MacBook Air. На машине x86_64 Linux setjmp заполняет структуру jmp_buf, которая имеет long[] содержащую «искаженные» значения регистров. Просматривая код glibc, я смог декодировать эти значения, например, чтобы получить указатель стека и указатель кадра.
На моем M1 MackBook Air этот jmp_buf тип кажется int[37] согласно lldb. Я вижу значения и печатаю их, но ни одно из них не соответствует указателю стека или указателю кадра, хотя некоторые из них близки.
Я ищу, как декодировать массив macOS M1 jmp_buf и получить указатель стека и указатель кадра. Также приветствуется любой исходный код. До сих пор я просматривал glibc , в частности каталог sysdeps/aarch64 ( каталог x86_64 был тем, что позволял мне декодировать на моей машине с Linux), и это зеркало открытого исходного кода Apple. Ни одна из структур jmp_buf не соответствует, и я не смог определить, происходит ли искажение/манинг.
У меня есть:
#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;
}
Что печатает что-то вроде:
% ./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
Я ожидаю, что в массиве jmp_buf будет значение, которое находится всего в нескольких байтах от адреса foo. Смещение № 4 при интерпретации как массив long приближается, но дальше, чем я ожидал.
Я ищу определения смещения и любую разборку значений, которая должна произойти.





О, это коварно на многих уровнях.
Для начала: ваш код работает в Rosetta.
Мое обоснованное предположение состоит в том, что вы используете либо IDE (VS Code?), либо эмулятор терминала x86_64, из которого вы вызываете компилятор, который затем также будет работать как x86_64, без явного флага целевой арки, что сделает его по умолчанию x86_64. Используйте -arch arm64 для cc/c++/gcc/g++/clang/clang++ для явного нацеливания на arm64 или добавьте к вызову компилятора префикс arch -arm64 [...], чтобы запустить всю иерархию процессов изначально.
Итак, как я определил, что ваш код работает под Rosetta? Это то, что Apple называет «манипулированием указателями». Таким образом, официальные дампы исходного кода Apple размещаются на github.com/apple-oss-distributions , а setjmp и longjmp реализованы в src/setjmp в libplatform, с реализованными вручную сборками для каждой архитектуры. Реализация arm64 такова:
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
Он довольно интенсивно использует макросы, поэтому вот необработанная дизассемблирование _longjmp из /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
Таким образом, регистры fp, lr и sp хранятся по смещениям 0x50, 0x58 и 0x60, но они также подвергаются операции XOR со значением, загруженным из [tpidrro_el0, 0x38]. Определения этих MUNGE макросов можно найти в xnu/libsyscall/os/tsd.h, но они на самом деле не говорят вам больше, чем [tpidrro_el0, 0x38]. Это просто файл cookie для каждого процесса, который подвергается операции XOR с этими значениями. Что выглядит так, если ваш код работает на 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
Обратите внимание, что старшие биты по смещениям 10 и 12 идентичны? Это связано с тем, что старшие биты в этих регистрах обычно равны нулю в пользовательской среде, поэтому, если вы выполните операцию XOR с 64-битной константой, старшие биты будут одинаковыми. Это совсем не то, что я вижу в вашем jmp_buf дампе. Ваши значения в этих индексах больше похожи на битовые маски. Однако я вижу это в индексах 1 и 2. Именно так работает реализация x86_64:
;-- __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
Так что да, я так и знал. Но вернемся к моему дампу arm64 выше, если мы посмотрим на сборку, то мы ожидаем, что не только индексы 10 и 12 будут иметь одинаковые старшие биты, но и индекс 11 (lr), но это не так. Так что там происходит?
Что ж, оказывается, у нас тоже не совсем версия для arm64. Мы запускаем arm64e! Если для вас это ничего не значит, это отдельный Apple ABI с поддержкой аутентификации указателя ARMv8.3. Загрузчики Mach-O всегда будут предпочитать слайсы arm64e, а не arm64, если аппаратное обеспечение поддерживает это, и, поскольку все Apple Silicon Mac делают это, и поскольку все стандартные бинарные файлы поставляются с фрагментом arm64e, libsystem_platform.dylib всегда будет загружен его слайс arm64e (если вам не удастся вручную возиться с ним достаточно, может быть?). В любом случае, вот реальные реализации _setjmp и _longjmp, которые действительно работают:
;-- __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
На данный момент это не похоже на открытый исходный код. И если я должен был предположить, это, вероятно, также не стабильный ABI. Вся подархитектура arm64e считается нестабильной, может быть изменена без предварительного уведомления, а в macOS требуется загрузочный аргумент ядра -arm64e_preview_abi (что, в свою очередь, требует пониженной безопасности ОС), чтобы вам даже было разрешено запускать исполняемые файлы arm64e, не подписанные Apple. Так что да, просто не полагайтесь на то, что этот код останется прежним.
Но хорошо, lr отличается из-за аутентификации указателя. setjmp делает pacibsp, а затем longjmp делает соответствующее retab. Пока все хорошо, за исключением того, что у fp и sp также есть свои pacdb и autdb, которые точно так же должны добавить аутентификацию по указателю. Но вот еще одна маленькая деталь: если вы запускаете бинарный файл arm64 на оборудовании с поддержкой arm64e, то в вашем процессе будут загружены библиотеки arm64e, но вы все равно работаете как arm64. Инструкции по аутентификации указателя отключены для вашего процесса с помощью аппаратных флагов в SCTLR_EL1, за исключением ключей IB. Таким образом, семейство инструкций pacib* будет работать, а pacia*, pacda* и pacdb* — нет. Но при чтении значений fp/lr/sp самостоятельно, в основном, вы захотите удалить биты аутентификации указателя и позволить аппаратному обеспечению решить, является ли это недопустимым или нет.
Итак, вот некоторый код, который печатает эти три регистра на arm64, на macOS 13.4.1, без гарантий прямой или обратной совместимости:
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
/* ---------- imported from xnu/libsyscall/tsd ---------- */
#define __TSD_PTR_MUNGE 7
__attribute__((always_inline, const)) static __inline__ void** _os_tsd_get_base(void)
{
#if defined(__arm__)
uintptr_t tsd;
__asm__("mrc p15, 0, %0, c13, c0, 3\n"
"bic %0, %0, #0x3\n" : "=r" (tsd));
/* lower 2-bits contain CPU number */
#elif defined(__arm64__)
/*
* <rdar://73762648> Do not use __builtin_arm_rsr64("TPIDRRO_EL0")
* so that the "const" attribute takes effect and repeated use
* is coalesced properly.
*/
uint64_t tsd;
__asm__ ("mrs %0, TPIDRRO_EL0" : "=r" (tsd));
#endif
return (void**)(uintptr_t)tsd;
}
__attribute__((always_inline)) static __inline__ void* _os_tsd_get_direct(unsigned long slot)
{
return _os_tsd_get_base()[slot];
}
__attribute__((always_inline, const)) static __inline__ uintptr_t _os_ptr_munge_token(void)
{
return (uintptr_t)_os_tsd_get_direct(__TSD_PTR_MUNGE);
}
/* ---------- end of import ---------- */
static inline uint64_t xpaci(uint64_t val)
{
__asm__ volatile
(
".arch v8.3a\n"
"xpaci %0\n"
: "+r" (val)
);
return val;
}
static inline uint64_t xpacd(uint64_t val)
{
__asm__ volatile
(
".arch v8.3a\n"
"xpacd %0\n"
: "+r" (val)
);
return val;
}
int main(void)
{
jmp_buf reg;
setjmp(reg);
uintptr_t token = _os_ptr_munge_token();
uint64_t *u64 = (uint64_t*)reg;
uint64_t fp = u64[10] ^ token,
lr = u64[11] ^ token,
sp = u64[12] ^ token;
printf("fp: 0x%llx\n", xpacd(fp));
printf("lr: 0x%llx\n", xpaci(lr));
printf("sp: 0x%llx\n", xpacd(sp));
return 0;
}
Примечание 1: обычно инструкции xpaci и xpacd не принимаются ассемблером, если они нацелены на arm64, но с помощью директивы .arch v8.3a мы можем убедить его пропустить нас.
Примечание 2: этот код будет работать только на оборудовании ARMv8.3 из-за инструкций xpaci и xpacd. Если ваш код может работать на оборудовании, которое не поддерживает PAC (например, на устройствах iOS до A12), вам необходимо изменить их. Для xpaci можно создать обратно совместимый вариант с xpaclri, который закодирован в историческом пространстве инструкций NOP (см. другой мой ответ), но для xpacd нет эквивалента в пространстве NOP, поэтому вам нужно сначала определить, на каком оборудовании вы работаете (с помощью таких вещей, как xpaclri), а затем условно вызвать гаджеты проверки подлинности указателя. Но это выходит за рамки этого ответа. :)
Это коварно! Спасибо за исчерпывающий ответ и ссылки на исходный код! Я бы никогда не понял компонент аутентификации указателя, даже если бы в конце концов понял, что Rosetta привыкает.