Можете ли вы ввести 32-разрядный "длинный совместимый подрежим совместимости с 64" вне режима ядра?

Это может быть точный дубликат Возможно ли выполнить 32-разрядный код в 64-разрядном процессе путем переключения режима?, но этот вопрос относится к году назад и имеет только один ответ, который не работает 't дать какой-либо исходный код. Я надеюсь получить более подробные ответы.

Я запускаю 64-разрядную Linux (Ubuntu 12.04, если это имеет значение). Здесь некоторый код, который выделяет страницу, записывает в нее 64-битный код и выполняет этот код.

#include <assert.h>
#include <malloc.h>
#include <stdio.h>
#include <sys/mman.h>  // mprotect
#include <unistd.h>  // sysconf

unsigned char test_function[] = { 0xC3 };  // RET
int main()
{
    int pagesize = sysconf(_SC_PAGE_SIZE);
    unsigned char *buffer = memalign(pagesize, pagesize);
    void (*func)() = (void (*)())buffer;

    memcpy(buffer, test_function, sizeof test_function);

    // func();  // will segfault 
    mprotect(buffer, pagesize, PROT_EXEC);
    func();  // works fine
}

Теперь, исключительно для развлекательной ценности, я хотел бы сделать то же самое, но с buffer, содержащим произвольный 32-разрядный (ia32) код вместо 64-битного кода. Эта страница подразумевает, что вы можете выполнить 32-разрядный код на 64-разрядном процессоре, введя "долговременный под-режим совместимости", установив бит дескриптора сегмента CS как LMA=1, L=0, D=1. Я готов обернуть свой 32-битный код в прологе/эпилоге, который выполняет эту настройку.

Но могу ли я сделать эту настройку в Linux в usermode? (Ответы BSD/Darwin также будут приняты.) Здесь я начинаю получать очень туманные понятия. Я думаю, что решение включает в себя добавление нового дескриптора сегмента в GDT (или это LDT?), А затем переход на этот сегмент с помощью команды lcall. Но может ли это быть сделано в usermode?

Здесь примерная функция, которая должна возвращать 4 при успешном запуске в подрежиме совместимости и 8 при работе в длинном режиме. Моя цель - заставить указатель инструкции взять эту кодировку и выйти с другой стороны %rax=4, не переходя в режим ядра (или делать это только с помощью документированных системных вызовов).

unsigned char behave_differently_depending_on_processor_mode[] = {
    0x89, 0xE0,  // movl %esp, %eax
    0x56,        // push %{e,r}si
    0x29, 0xE0,  // subl %esp, %eax
    0x5E,        // pop %{e,r}si
    0xC3         // ret
};

Ответ 1

Да, ты можешь. Это даже выполнимо, используя полностью поддерживаемые интерфейсы. Используйте modify_ldt для установки 32-битного сегмента кода в LDT, затем установите дальний указатель на ваш 32-битный код, а затем сделайте косвенный переход к нему, используя ljumpl *(%eax) в нотации AT & T.

Вы столкнетесь со всеми видами снафуса, хотя. Старшие биты вашего указателя стека могут быть уничтожены. Вам, вероятно, нужен сегмент данных, если вы действительно хотите запустить реальный код. И вам нужно будет сделать еще один прыжок, чтобы вернуться в 64-битный режим.

Полностью проработанный пример приведен в моих linux-clock-tests в test_vsyscall.cc. (Это немного сломано на любом выпущенном ядре: int cc вылетит. Вы должны изменить это на что-то более умное, например, "nop". Смотрите в intcc32.