GNU/Linux >> Znalost Linux >  >> Linux

Jak vyvolat systémové volání přes syscall nebo sysenter v inline sestavení?

Explicitní proměnné registru

https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Explicit-Register-Variables.html#Explicit-Reg-Vars)

Věřím, že toto by nyní měl být obecně doporučený přístup k omezením registru, protože:

  • může reprezentovat všechny registry, včetně r8 , r9 a r10 které se používají pro argumenty systémového volání:Jak zadat omezení registru v registru Intel x86_64 r8 až r15 v sestavení GCC?
  • je to jediná optimální možnost pro ostatní ISA kromě x86, jako je ARM, které nemají názvy omezení magického registru:Jak specifikovat individuální registr jako omezení v inline sestavě ARM GCC? (kromě použití dočasného registru + clobbers + a další instrukce mov)
  • Budu tvrdit, že tato syntaxe je čitelnější než použití jednopísmenných mnemotechnických pomůcek, jako je S -> rsi

Proměnné registru se používají například v glibc 2.29, viz:sysdeps/unix/sysv/linux/x86_64/sysdep.h .

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)
        : "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)
        : "rcx", "r11", "memory"
    );
}

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

GitHub upstream.

Kompilace a spuštění:

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

Výstup

hello world
0

Pro srovnání, následující analogie k Jak vyvolat systémové volání přes syscall nebo sysenter v inline sestavení? vytváří ekvivalentní sestavu:

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)
        : "rcx", "r11", "memory"
    );
    return ret;
}

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

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

GitHub upstream.

Demontáž obou pomocí:

objdump -d main_reg.out

je téměř identický, zde je main_reg.c jeden:

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   

Vidíme tedy, že GCC vložilo tyto drobné funkce syscall tak, jak by bylo žádoucí.

my_write a my_exit jsou stejné pro oba, ale _start v main_constraint.c je trochu jiný:

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 

Je zajímavé pozorovat, že v tomto případě GCC nalezlo o něco kratší ekvivalentní kódování výběrem:

    104b:   89 c7                   mov    %eax,%edi

nastavte fd na 1 , což se rovná 1 z čísla systémového volání, spíše než z přímějšího:

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

Podrobnou diskuzi o konvencích volání viz také:Jaké jsou konvence volání pro systémová volání UNIX a Linux (a funkce v uživatelském prostoru) na i386 a x86-64

Testováno v Ubuntu 18.10, GCC 8.2.0.


Za prvé, nemůžete bezpečně používat GNU C Basic asm(""); syntaxe pro toto (bez omezení vstup/výstup/clobber). Potřebujete Extended asm, abyste řekli kompilátoru o registrech, které upravujete. Podívejte se na inline asm v příručce GNU C a na wiki inline-assembly tag, kde najdete odkazy na další průvodce, kde najdete podrobnosti o tom, co jako "D"(1) znamená jako součást asm() prohlášení.

Potřebujete také asm volatile protože to není implicitní pro Extended asm příkazy s 1 nebo více výstupními operandy.

Ukážu vám, jak provádět systémová volání napsáním programu, který zapíše Hello World! na standardní výstup pomocí write() systémové volání. Zde je zdroj programu bez implementace skutečného systémového volání:

#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;
}

Můžete vidět, že jsem pojmenoval svou vlastní funkci systémového volání jako my_write aby se předešlo kolizím jmen s "normálním" write , poskytuje libc. Zbytek této odpovědi obsahuje zdroj my_write pro i386 a amd64.

i386

Systémová volání v i386 Linuxu jsou implementována pomocí 128. vektoru přerušení, např. voláním int 0x80 v kódu sestavy, samozřejmě s tím, že jste předem odpovídajícím způsobem nastavili parametry. Totéž je možné provést pomocí SYSENTER , ale skutečné provedení této instrukce je dosaženo virtuálním mapováním VDSO ke každému běžícímu procesu. Od SYSENTER nebylo nikdy míněno jako přímá náhrada int 0x80 API, nikdy ho přímo nespouštějí uživatelské aplikace – místo toho, když aplikace potřebuje získat přístup k nějakému kódu jádra, zavolá virtuálně mapovanou rutinu ve VDSO (to je to, co call *%gs:0x10 ve vašem kódu je for), který obsahuje veškerý kód podporující SYSENTER návod. Je toho docela hodně kvůli tomu, jak instrukce ve skutečnosti funguje.

Pokud si o tom chcete přečíst více, podívejte se na tento odkaz. Obsahuje poměrně stručný přehled technik používaných v jádře a VDSO. Viz také The Definitive Guide to (x86) Linux System Calls – některá systémová volání jako getpid a clock_gettime jsou tak jednoduché, že jádro dokáže exportovat kód + data, která běží v uživatelském prostoru, takže VDSO nikdy nemusí vstupovat do jádra, takže je mnohem rychlejší než sysenter může být.

Je mnohem jednodušší použít pomalejší int $0x80 k vyvolání 32bitového ABI.

// i386 Linux
#include <asm/unistd.h>      // compile with -m32 for 32 bit call numbers
//#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)
        : "memory"    // the kernel dereferences pointer args
    );
    return ret;
}

Jak můžete vidět, pomocí int 0x80 API je poměrně jednoduché. Číslo systémového volání přejde na eax registr, zatímco všechny parametry potřebné pro systémové volání jdou do ebx , ecx , edx , esi , edi a ebp . Čísla systémových volání lze získat přečtením souboru /usr/include/asm/unistd_32.h .

Prototypy a popisy funkcí jsou k dispozici ve 2. části manuálu, takže v tomto případě write(2) .

Jádro ukládá/obnovuje všechny registry (kromě EAX), takže je můžeme použít jako operandy pouze pro vstup do inline asm. Viz Jaké jsou konvence volání pro systémová volání UNIX a Linux (a funkce v uživatelském prostoru) na i386 a x86-64

Pamatujte, že seznam clobberů obsahuje také memory parametr, což znamená, že instrukce uvedená v seznamu instrukcí odkazuje na paměť (prostřednictvím buf parametr). (Vstup ukazatele na inline asm neznamená, že paměť, na kterou ukazuje, je také vstupem. Viz Jak mohu naznačit, že lze použít paměť, na kterou *ukazuje* vložený argument ASM?)

amd64

Věci vypadají jinak na architektuře AMD64, která obsahuje novou instrukci nazvanou SYSCALL . Velmi se liší od původního SYSENTER instrukce a rozhodně mnohem jednodušší na použití z uživatelských aplikací - opravdu připomíná normální CALL , ve skutečnosti a přizpůsobení starého int 0x80 na nový SYSCALL je docela triviální. (Kromě toho používá RCX a R11 místo zásobníku jádra k uložení RIP a RFLAGS v uživatelském prostoru, aby jádro vědělo, kam se má vrátit).

V tomto případě je číslo systémového volání stále předáno v registru rax , ale registry používané k uložení argumentů nyní téměř odpovídají konvenci volání funkcí:rdi , rsi , rdx , r10 , r8 a r9 v tomto pořadí. (syscall sám zničí rcx takže r10 se používá místo rcx , takže funkce libc wrapper budou používat pouze mov r10, rcx / syscall .)

// x86-64 Linux
#include <asm/unistd.h>      // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "syscall"
        : "=a" (ret)
        //                 EDI      RSI       RDX
        : "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
        : "rcx", "r11", "memory"
    );
    return ret;
}

(Viz kompilaci na Godboltu)

Všimněte si, jak prakticky jedinou věcí, kterou bylo potřeba změnit, byly názvy registrů a skutečné instrukce použité pro uskutečnění hovoru. To je většinou díky seznamům vstupů/výstupů poskytovaných rozšířenou syntaxí inline sestavení gcc, která automaticky poskytuje příslušné instrukce přesunu potřebné pro provedení seznamu instrukcí.

"0"(callnum) odpovídající omezení lze zapsat jako "a" protože operand 0 ("=a"(ret) výstup) má k výběru pouze jeden registr; víme, že vybere EAX. Použijte cokoli, co vám přijde jasnější.

Všimněte si, že nelinuxové operační systémy, jako je MacOS, používají různá telefonní čísla. A dokonce i různé konvence předávání argumentů pro 32bitové verze.


Linux
  1. Jak změnit název hostitele v systému Linux

  2. Jak upgradovat balíčky na Ubuntu pomocí příkazového řádku

  3. Tabulka systémových volání Linux nebo cheatsheet pro shromáždění

  1. Jak vytisknout číslo v sestavě NASM?

  2. Nejrychlejší systémové volání Linuxu

  3. Jak předat parametry systémovému volání Linuxu?

  1. Jak nakonfigurovat virtualizaci na Redhat Linuxu

  2. Jak mmapovat zásobník pro systémové volání clone() na linuxu?

  3. x86_64 Assembly Linux System Call Zmatek