Linux системный вызов, libc, VDSO и анализ реализации

Я анализирую вызов syscall в последнем libc:

git clone git://sourceware.org/git/glibc.git

И у меня есть этот код в sysdeps/unix/sysv/linux/i386/sysdep.h:

#   define INTERNAL_SYSCALL_MAIN_INLINE(name, err, nr, args...) \
LOADREGS_##nr(args)                         \
asm volatile (                          \
"call *%%gs:%P2"                            \
: "=a" (resultvar)                          \
: "a" (__NR_##name), "i" (offsetof (tcbhead_t, sysinfo))        \
  ASMARGS_##nr(args) : "memory", "cc")

Если я хорошо понимаю этот код, макрос LOADREGS_##nr(args) загружает аргумент в регистры ebx, ecx, edx, esi, edx и ebp.

sysdeps / Unix / SysV / Linux / i386 / наш заголовочный файл sysdep.h

# define LOADREGS_0()
# define ASMARGS_0()
# define LOADREGS_1(arg1) \
    LOADREGS_0 ()
# define ASMARGS_1(arg1) \
    ASMARGS_0 (), "b" ((unsigned int) (arg1))
# define LOADREGS_2(arg1, arg2) \
    LOADREGS_1 (arg1)
# define ASMARGS_2(arg1, arg2) \
    ASMARGS_1 (arg1), "c" ((unsigned int) (arg2))
# define LOADREGS_3(arg1, arg2, arg3) \
    LOADREGS_2 (arg1, arg2)
# define ASMARGS_3(arg1, arg2, arg3) \
    ASMARGS_2 (arg1, arg2), "d" ((unsigned int) (arg3))
# define LOADREGS_4(arg1, arg2, arg3, arg4) \
    LOADREGS_3 (arg1, arg2, arg3)
# define ASMARGS_4(arg1, arg2, arg3, arg4) \
    ASMARGS_3 (arg1, arg2, arg3), "S" ((unsigned int) (arg4))
# define LOADREGS_5(arg1, arg2, arg3, arg4, arg5) \
    LOADREGS_4 (arg1, arg2, arg3, arg4)
# define ASMARGS_5(arg1, arg2, arg3, arg4, arg5) \
    ASMARGS_4 (arg1, arg2, arg3, arg4), "D" ((unsigned int) (arg5))
# define LOADREGS_6(arg1, arg2, arg3, arg4, arg5, arg6) \
    register unsigned int _a6 asm ("ebp") = (unsigned int) (arg6); \
    LOADREGS_5 (arg1, arg2, arg3, arg4, arg5)
# define ASMARGS_6(arg1, arg2, arg3, arg4, arg5, arg6) \
    ASMARGS_5 (arg1, arg2, arg3, arg4, arg5), "r" (_a6)
#endif /* GCC 5  */
    enter code here

Где код, который загружает аргументы в регистры ebx, ecx, edx, esi, edx и ebp? это код выше? Я не понимаю реализацию. следующий код загружает 6-й аргумент в регистр ebx?

register unsigned int _a6 asm ("ebp") = (unsigned int) (arg6);

Что означает этот код:

ASMARGS_0 (), "b" ((unsigned int) (arg1))

Он загружает первый аргумент в регистр ebx?

Затем "вызов *%%gs:%P2" перейти к коду VDSO? этот код соответствует "call *gs:0x10"?

Итак, эта следующая диаграмма для записи системного вызова, это хорошо?:

write(1, "A", 1)  ----->   LIBC   ----->   VDSO   -----> KERNEL
                          load reg           ?   
                        jump to vdso 
|---------------------------------------------------|--------------|
       user land                                       kernel land

Я не понимаю утилиту VDSO! vdso выбирает метод системного вызова (sysenter или int 0x80).

Заранее благодарю за помощь. И извините, мой английский очень плох.

1 ответ

Макросы, включенные в системные вызовы glibc, будут расширены до чего-то вроде следующего, например, для системного вызова exit.

LOADREGS_1(args)
asm volatile (
"call *%%gs:%P2"
: "=a" (resultvar)
: "a" (__NR_exit), "i" (offsetof (tcbhead_t, sysinfo))
  ASMARGS_1(args) : "memory", "cc")

LOADREGS_1(args) будет расширяться до LOADREGS_0(), который будет расширяться до нуля - LOADREGS_*(...) регулировать регистры нужно только тогда, когда предоставлено больше параметров.

ASMARGS_1(args) будет расширяться до ASMARGS_0 (), "b" ((unsigned int) (arg1)), который будет расширяться до , "b" ((unsigned int) (arg1),

__NR_exit 1 на х86.

Таким образом, код расширится до чего-то вроде:

asm volatile (
"call *%%gs:%P2"
: "=a" (resultvar)
: "a" (1), "i" (offsetof (tcbhead_t, sysinfo))
, "b" ((unsigned int) (arg1) : "memory", "cc")

ASMARGS_* на самом деле не выполняйте код как таковой - это инструкции gcc чтобы убедиться, что определенные значения (такие как (unsigned int) (arg1)) находятся в определенных регистрах (таких как bака ebx). Таким образом, сочетание параметров для asm volatile (что, конечно, не функция, а встроенный gcc), просто укажите, как gcc следует подготовиться к системному вызову и как это должно продолжаться после завершения системного вызова.

Теперь сгенерированная сборка будет выглядеть примерно так:

; set up other registers...
movl $1, %eax
call *%gs:0x10
; tear down

%gs это сегментный регистр, который ссылается на локальное хранилище потока - в частности, glibc ссылается на сохраненное значение, которое указывает на VDSO, которое он сохранял там, когда впервые анализировал заголовки ELF, сообщающие ему, где находится VDSO.

Как только код поступает в VDSO, мы не знаем точно, что происходит - он меняется в зависимости от версии ядра - но мы знаем, что он использует наиболее эффективный доступный механизм для запуска системного вызова, такой как sysenter инструкция или int 0x80 инструкция.

Итак, да, ваша диаграмма точна:

write(1, "A", 1)  ----->   LIBC   ----->   VDSO   -----> KERNEL
                          load reg           ?   
                        jump to vdso 
|---------------------------------------------------|--------------|
       user land                                       kernel land

Вот более простой пример кода для вызова в VDSO, специально для однопараметрических системных вызовов, из библиотеки, которую я поддерживаю под названием libsyscall:

_lsc_syscall1:
    xchgl 8(%esp), %ebx
    movl 4(%esp), %eax
    call *_lsc_vdso_ptr(,1)
    movl 8(%esp), %ebx
    # pass %eax out
    ret

Это просто перемещает параметры из стека в регистры, вызывает в VDSO через указатель, загруженный из памяти, восстанавливает другие регистры в их предыдущее состояние и возвращает результат системного вызова.

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