Как декодировать jmp_buf на M1 Mac?

Я играю с 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 приближается, но дальше, чем я ожидал.

Я ищу определения смещения и любую разборку значений, которая должна произойти.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
79
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

О, это коварно на многих уровнях.

Для начала: ваш код работает в 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 привыкает.

Sam 23.07.2023 14:59

Другие вопросы по теме