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
ar10
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.