Как вызвать системный вызов через 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/. Но все же трудно разобраться.

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 никогда не задумывался как прямая замена int 0x80 API, он никогда не выполняется напрямую пользовательскими приложениями - вместо этого, когда приложению требуется доступ к некоторому коду ядра, он вызывает виртуально сопоставленную подпрограмму в 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;
}

Как вы можете видеть, используя int 0x80 API относительно прост. Номер системного вызова отправляется на eax зарегистрироваться, в то время как все параметры, необходимые для системного вызова, входят соответственно ebx, ecx, edx, esi, edi, а также ebp, Номера системных вызовов можно получить, прочитав файл /usr/include/asm/unistd_32.h, Прототипы и описания функций доступны во 2-м разделе руководства, поэтому в этом случае write(2), Поскольку ядру разрешено уничтожать практически любой из регистров, я включил все оставшиеся GPR в список клоббера, а также 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, который автоматически предоставляет соответствующие инструкции перемещения, необходимые для выполнения списка инструкций.

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

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

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

  • может представлять все регистры, включая r8, r9 а также r10 которые используются для аргументов системного вызова
  • Я буду утверждать, что этот синтаксис более читабелен, чем использование однобуквенной мнемоники, такой как S -> rsi

Переменные регистра используются, например, в 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.

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