Как вызвать системный вызов через sysenter во встроенной сборке? - программирование
Подтвердить что ты не робот

Как вызвать системный вызов через sysenter во встроенной сборке?

Как мы можем реализовать системный вызов с помощью sysenter/syscall непосредственно в x86 Linux? Кто-нибудь может помочь? Было бы еще лучше, если бы вы также могли показать код для платформы amd64.

Я знаю в x86, мы можем использовать

__asm__(
"               movl $1, %eax  \n"
"               movl $0, %ebx \n"
"               call *%gs:0x10 \n"
);

для косвенной маршрутизации в sysenter.

Но как мы можем с помощью sysenter/syscall напрямую запрограммировать системный вызов?

Я нахожу материал http://damocles.blogbus.com/tag/sysenter/. Но все еще трудно понять.

4b9b3361

Ответ 1

Я расскажу вам, как выполнять системные вызовы, написав программу, которая записывает Hello World! в стандартный вывод с помощью системного вызова write(). Здесь источник программы без реализации фактического системного вызова:

#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size);

int main(void)
{
    const char hello[] = "Hello world!\n";
    my_write(1, hello, sizeof(hello));
    return 0;
}

Вы можете видеть, что я назвал свою пользовательскую функцию системного вызова как my_write, чтобы избежать столкновений имен с "нормальным" write, предоставленным libc. Остальная часть этого ответа содержит источник my_write для i386 и amd64.

i386

Системные вызовы в i386 Linux реализованы с использованием 128-го вектора прерывания, например. вызвав int 0x80 в вашем ассемблере, предварительно задав параметры заранее. Можно сделать то же самое через SYSENTER, но фактически выполнение этой команды достигается посредством VDSO, фактически сопоставленного каждому выполняющемуся процессу. Поскольку SYSENTER никогда не подразумевалось как прямая замена API int 0x80, он никогда не выполнялся непосредственно приложениями userland - вместо этого, когда приложение должно получить доступ к некоторому коду ядра, оно вызывает фактически отображаемую процедуру в VDSO (что call *%gs:0x10 в вашем коде для), который содержит весь код, поддерживающий инструкцию SYSENTER. Там очень много из-за того, как на самом деле работает инструкция.

Если вы хотите больше узнать об этом, посмотрите эту ссылку. Он содержит довольно краткий обзор методов, применяемых в ядре и VDSO.

#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "int $0x80"
        : "=a" (ret)
        : "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
        : "cc", "edi", "esi", "memory"
    );
    return ret;
}

Как вы можете видеть, использование API int 0x80 относительно прост. Номер syscall переходит в регистр eax, а все параметры, необходимые для syscall, идут соответственно ebx, ecx, edx, esi, edi и ebp. Номера системных вызовов можно получить, прочитав файл /usr/include/asm/unistd_32.h. Прототипы и описания функций доступны во втором разделе руководства, поэтому в данном случае write(2). Поскольку ядру разрешено уничтожать практически любой из регистров, я помещаю все остальные GPR в список clobber, а также cc, так как регистр eflags также может измениться. Имейте в виду, что список clobber также содержит параметр memory, что означает, что команда, указанная в списке команд, ссылается на память (через параметр buf).

amd64

В архитектуре AMD64 все выглядит очень по-разному: новая инструкция называется SYSCALL. Он сильно отличается от оригинальной инструкции SYSENTER и, безусловно, гораздо проще в использовании из пользовательских приложений - он действительно похож на обычный CALL, на самом деле, и адаптация старого int 0x80 к новому SYSCALL довольно тривиальна.

В этом случае номер системного вызова по-прежнему передается в регистре rax, но регистры, используемые для хранения аргументов, сильно изменились, так как теперь они должны использоваться в следующем порядке: rdi, rsi, rdx, r10, r8 и r9. Ядру разрешено уничтожать содержимое регистров rcx и r11 (они используются для сохранения некоторых других регистров на SYSCALL).

#define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "syscall"
        : "=a" (ret)
        : "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
        : "cc", "rcx", "r11", "memory"
    );
    return ret;
}

Обратите внимание, как практически единственное, что нужно было изменить, это имена регистров и фактическая инструкция, используемая для совершения вызова. Это в основном благодаря спискам ввода/вывода, предоставляемым gcc расширенным синтаксисом встроенной сборки, который автоматически предоставляет соответствующие инструкции перемещения, необходимые для выполнения списка инструкций.

Ответ 2

Явные переменные регистра

Просто для полноты я хочу привести пример использования явных регистровых переменных GCC.

Этот механизм имеет следующие преимущества:

Переменные регистра используются, например, в glibc 2.29, см. sysdeps/unix/sysv/linux/x86_64/sysdep.h.

Также обратите внимание, что другие арки, такие как ARM, полностью отбросили однобуквенную мнемонику, и переменные регистров - единственный способ сделать это, см., Например: Как указать отдельный регистр в качестве ограничения в встроенной сборке ARM GCC?

main_reg.c

#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size) {
    register int64_t rax __asm__ ("rax") = 1;
    register int rdi __asm__ ("rdi") = fd;
    register const void *rsi __asm__ ("rsi") = buf;
    register size_t rdx __asm__ ("rdx") = size;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi), "r" (rsi), "r" (rdx)
        : "cc", "rcx", "r11", "memory"
    );
    return rax;
}

void my_exit(int exit_status) {
    register int64_t rax __asm__ ("rax") = 60;
    register int rdi __asm__ ("rdi") = exit_status;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi)
        : "cc", "rcx", "r11", "memory"
    );
}

void _start(void) {
    char msg[] = "hello world\n";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub вверх по течению.

Скомпилируйте и запустите:

gcc -O3 -std=c99 -ggdb3 -ffreestanding -nostdlib -Wall -Werror \
  -pedantic -o main_reg.out main_reg.c
./main.out
echo $?

Выход

hello world
0

Для сравнения приведено следующее, аналогичное Как вызвать системный вызов через sysenter во встроенной сборке? производит эквивалентную сборку:

main_constraint.c

#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size) {
    ssize_t ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "0" (1), "D" (fd), "S" (buf), "d" (size)
        : "cc", "rcx", "r11", "memory"
    );
    return ret;
}

void my_exit(int exit_status) {
    ssize_t ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "0" (60), "D" (exit_status)
        : "cc", "rcx", "r11", "memory"
    );
}

void _start(void) {
    char msg[] = "hello world\n";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub вверх по течению.

Разборка обоих с:

objdump -d main_reg.out

почти идентичен, вот main_reg.c один:

Disassembly of section .text:

0000000000001000 <my_write>:
    1000:   b8 01 00 00 00          mov    $0x1,%eax
    1005:   0f 05                   syscall 
    1007:   c3                      retq   
    1008:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    100f:   00 

0000000000001010 <my_exit>:
    1010:   b8 3c 00 00 00          mov    $0x3c,%eax
    1015:   0f 05                   syscall 
    1017:   c3                      retq   
    1018:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    101f:   00 

0000000000001020 <_start>:
    1020:   c6 44 24 ff 00          movb   $0x0,-0x1(%rsp)
    1025:   bf 01 00 00 00          mov    $0x1,%edi
    102a:   48 8d 74 24 f3          lea    -0xd(%rsp),%rsi
    102f:   48 b8 68 65 6c 6c 6f    movabs $0x6f77206f6c6c6568,%rax
    1036:   20 77 6f 
    1039:   48 89 44 24 f3          mov    %rax,-0xd(%rsp)
    103e:   ba 0d 00 00 00          mov    $0xd,%edx
    1043:   b8 01 00 00 00          mov    $0x1,%eax
    1048:   c7 44 24 fb 72 6c 64    movl   $0xa646c72,-0x5(%rsp)
    104f:   0a 
    1050:   0f 05                   syscall 
    1052:   31 ff                   xor    %edi,%edi
    1054:   48 83 f8 0d             cmp    $0xd,%rax
    1058:   b8 3c 00 00 00          mov    $0x3c,%eax
    105d:   40 0f 95 c7             setne  %dil
    1061:   0f 05                   syscall 
    1063:   c3                      retq   

Итак, мы видим, что GCC встроил эти крошечные функции системного вызова так, как хотелось бы.

my_write и my_exit одинаковы для обоих, но _start в main_constraint.c немного отличается:

0000000000001020 <_start>:
    1020:   c6 44 24 ff 00          movb   $0x0,-0x1(%rsp)
    1025:   48 8d 74 24 f3          lea    -0xd(%rsp),%rsi
    102a:   ba 0d 00 00 00          mov    $0xd,%edx
    102f:   48 b8 68 65 6c 6c 6f    movabs $0x6f77206f6c6c6568,%rax
    1036:   20 77 6f 
    1039:   48 89 44 24 f3          mov    %rax,-0xd(%rsp)
    103e:   b8 01 00 00 00          mov    $0x1,%eax
    1043:   c7 44 24 fb 72 6c 64    movl   $0xa646c72,-0x5(%rsp)
    104a:   0a 
    104b:   89 c7                   mov    %eax,%edi
    104d:   0f 05                   syscall 
    104f:   31 ff                   xor    %edi,%edi
    1051:   48 83 f8 0d             cmp    $0xd,%rax
    1055:   b8 3c 00 00 00          mov    $0x3c,%eax
    105a:   40 0f 95 c7             setne  %dil
    105e:   0f 05                   syscall 
    1060:   c3                      retq 

Интересно отметить, что в этом случае GCC обнаружил немного более короткое эквивалентное кодирование, выбрав:

    104b:   89 c7                   mov    %eax,%edi

установить для fd значение 1, что равно 1 из числа системных вызовов, а не более прямым:

    1025:   bf 01 00 00 00          mov    $0x1,%edi    

Подробное обсуждение соглашений о вызовах см. Также: Каковы соглашения о вызовах для системных вызовов UNIX и Linux на i386 и x86-64?

Протестировано в Ubuntu 18.10, GCC 8.2.0.