Как вызвать системный вызов через 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));
}
Скомпилируйте и запустите:
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));
}
Разборка обоих с:
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.