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

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

Я использую 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 в пользовательском режиме? (Ответы BSD / Дарвина также будут приняты.) Вот здесь я начинаю очень расплываться в концепциях. Я думаю, что решение включает добавление нового дескриптора сегмента в GDT (или это LDT?), А затем переключение на этот сегмент с помощью инструкции lcall. Но можно ли все это сделать в пользовательском режиме?

Вот пример функции, которая должна возвращать 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
};

person Quuxplusone    schedule 03.10.2012    source источник
comment
Разве это не связано с en.wikipedia.org/wiki/X32_ABI x32? Или на тот же вопрос, на который дан ответ в stackoverflow.com/a/12712639/841108?   -  person Basile Starynkevitch    schedule 04.10.2012
comment
Мое использование mprotect решает вопрос, на который дан ответ на stackoverflow.com/a/12712639/841108 (как получить новую исполняемую страницу ); мой главный вопрос касается подрежима совместимости. Я считаю, что x32 ABI совершенно неуместен - x32 - это просто дурацкий ABI, используемый дурацкими системами в обычном 64-битном длинном режиме, тогда как я хочу фактически переключить декодер в подрежим 32-битной совместимости. (Другими словами, мой вопрос вообще не имеет отношения к ABI; он связан с режимом процессора.)   -  person Quuxplusone    schedule 04.10.2012
comment
Одна вещь, не упомянутая в другом вопросе, заключается в том, что для того, чтобы это работало, ваш буфер должен быть в 4 ГБ виртуальной памяти, так как остальная часть недоступна в 32-битном режиме. Если вы не можете этого гарантировать, ваш код будет в лучшем случае ненадежным. Инструкции по настройке GDT и LDT относятся только к ядру, поэтому, если ядро ​​уже не имеет 32-битного сегмента кода в известном месте или не предоставляет доступ к LDT, это невозможно. Я не знаю, предоставляет ли Linux что-либо из этих вещей, поэтому я не могу дать вам прямого ответа.   -  person ughoavgfhw    schedule 04.10.2012
comment
@ughoavgfhw Это хороший момент по поводу низких 4 ГБ (ну, на самом деле 2 ГБ или самые большие 2 ГБ). Обращение к статическим данным быстро станет уродливым. Но большая часть кода x86 оказывается позиционно-независимой, даже не пытаясь, так что давайте просто притворимся, что я могу каким-то образом гарантировать, что мой код является PIC. Что касается доступа к LDT, у Дарвина есть i386_set_ldt, а у Linux modify_ldt, но я не понимаю, что они делают.   -  person Quuxplusone    schedule 04.10.2012
comment
PIC недостаточно, ваш код и данные, включая стек, должны находиться в этой области, потому что все, включая указатель инструкции, будет усечено до 32 бит. Я имел в виду физическую способность делать это. Например, я знаю, что OS X резервирует эту область в 64-битных процессах, поэтому это невозможно. Что касается LDT, вам нужно будет установить дескрипторы как для кода, так и для данных. См. здесь для получения информации о формате дескриптора. После того, как вы настроите эту настройку и разместите свои данные в нужном месте, вам просто нужно lcall и установить дескрипторы данных (_2 _, _ 3_)   -  person ughoavgfhw    schedule 04.10.2012
comment
В Linux вы можете использовать mmap с флагами MAP_ANONYMOUS|MAP_32BIT для получения страницы в низкой (высокой) памяти. Что касается LDT, я все еще надеюсь, что кто-нибудь даст мне коды. Я думаю, что понимаю, что делают настоящие инструкции lgdt и lldt, но я думаю, что modify_ldt - это что-то более высокоуровневое, которое не уничтожает всю таблицу; вы можете как-то добавить новые записи в существующий LDT. Возможно, мне стоит изучить этот пример кода.   -  person Quuxplusone    schedule 05.10.2012


Ответы (1)


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

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

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

person Andy Lutomirski    schedule 13.11.2012
comment
Теоретически вы можете переключаться между 64- и 32-битным режимами используя существующие записи GDT ОС. Также позже в этой ветке комментариев Росс Ридж указывает, что modify_ldt не поддерживает установку бита L, поэтому он может не работать. (По крайней мере, не для 32-битного кода, который хочет переключиться из режима совместимости в длинный.) Я не тестировал код в этом ответе. - person Peter Cordes; 12.03.2016
comment
Если вы переходите с 64- ›32, вы должны использовать ljmpq *(%rax) или, может быть, ljmpl, но нет причин использовать 32-битный режим адресации для загрузки m16:64 или m16:32 цели дальнего перехода. - person Peter Cordes; 15.05.2019