Obecná část
EDIT:Linux irelevantní části odstraněny
I když to není úplně špatně, zúžení na int 0x80
a syscall
příliš zjednodušuje otázku jako u sysenter
existuje alespoň 3. možnost.
Použití 0x80 a eax pro číslo syscall, ebx, ecx, edx, esi, edi a ebp k předání parametrů je jen jednou z mnoha dalších možností implementace systémového volání, ale tyto registry si vybralo 32bitové Linuxové ABI. .
Než se blíže podíváme na příslušné techniky, je třeba uvést, že všechny krouží kolem problému útěku z privilegovaného vězení, se kterým každý proces běží.
Další možností k těm, které zde nabízí architektura x86, by bylo použití brány volání (viz:http://en.wikipedia.org/wiki/Call_gate)
Jedinou další možností na všech strojích i386 je použití softwarového přerušení, které umožňuje ISR (Interrupt Service Rutine nebo jednoduše obsluha přerušení ) spustit na jiné úrovni oprávnění než dříve.
(Zábavný fakt:některé operační systémy i386 použily výjimku neplatných instrukcí pro zadání jádra pro systémová volání, protože to bylo ve skutečnosti rychlejší než int
instrukce na 386 CPU. Podívejte se na instrukce OsDev syscall/sysret a sysenter/sysexit umožňující shrnutí možných mechanismů systémových volání.)
Softwarové přerušení
Co přesně se stane po spuštění přerušení, závisí na tom, zda přepnutí na ISR vyžaduje změnu oprávnění či nikoli:
(Příručka vývojáře softwaru Intel® 64 a IA-32 Architectures)
6.4.1 Operace volání a vrácení pro postupy zpracování přerušení nebo výjimek
...
Pokud má segment kódu pro proceduru handleru stejnou úroveň oprávnění jako aktuálně spouštěný program nebo úloha, procedura handleru použije aktuální zásobník; pokud handler provádí na amore privilegované úrovni, procesor se přepne do zásobníku pro úroveň privilegií handleru.
....
Pokud dojde k přepnutí zásobníku, procesor provede následující:
Dočasně (interně) uloží aktuální obsah registrů SS, ESP, EFLAGS, CS a> EIP.
Načte selektor segmentu a ukazatel zásobníku pro nový zásobník (tj. zásobník pro volanou úroveň oprávnění) z TSS do registrů SS a ESP a přepne na nový zásobník.
Vloží dočasně uložené hodnoty SS, ESP, EFLAGS, CS a EIP pro zásobník přerušené procedury do nového zásobníku.
Vloží chybový kód do nového zásobníku (pokud je to vhodné).
Načte selektor segmentu pro nový kódový segment a ukazatel nové instrukce (z hradla přerušení) do registrů CS a EIP.
Pokud je volání přes bránu přerušení, vymaže příznak IF v registru EFLAGS.
Zahájí provádění procedury obslužné rutiny na nové úrovni oprávnění.
... povzdechnout si, zdá se, že je toho hodně, a ani když skončíme, moc se to nezlepší:
(výňatek převzat ze stejného zdroje, jak je uvedeno výše:Intel® 64 and IA-32 Architectures Software Developer’s Manual)
Při provádění návratu z obsluhy přerušení nebo výjimky z jiné úrovně oprávnění, než je přerušená procedura, procesor provede tyto akce:
Provede kontrolu oprávnění.
Obnoví registry CS a EIP na jejich hodnoty před přerušením nebo výjimkou.
Obnoví registr EFLAGS.
Obnoví registry SS a ESP na jejich hodnoty před přerušením nebo výjimkou, což vede k přepnutí zásobníku zpět do zásobníku přerušené procedury.
Obnoví provádění přerušené procedury.
Sysenter
Další možností na 32bitové platformě, která se ve vaší otázce vůbec nezmiňuje, ale přesto ji jádro Linuxu využívá, je sysenter
instrukce.
(Příručka vývojáře softwaru Intel® 64 a IA-32 Architectures, díl 2 (2A, 2B a 2C):Reference sady instrukcí, A-Z)
Popis Provede rychlé volání systémové procedury nebo rutiny úrovně 0. SYSENTER je doprovodná instrukce k SYSEXIT. Pokyny jsou optimalizovány tak, aby poskytovaly maximální výkon pro systémová volání od uživatelského kódu spuštěného na úrovni oprávnění 3 až po operační systém nebo výkonné procedury spuštěné na úrovni oprávnění 0.
Jednou nevýhodou použití tohoto řešení je, že není přítomno na všech 32bitových počítačích, takže int 0x80
stále musí být poskytnuta metoda pro případ, že o ní CPU neví.
Instrukce SYSENTER a SYSEXIT byly zavedeny do architektury IA-32 v procesoru Pentium II. Dostupnost těchto instrukcí na procesoru je indikována příznakem funkce SYSENTER/SYSEXITpresent (SEP) vráceným do registru EDX instrukcí CPUID. Operační systém, který kvalifikuje příznak SEP, musí také kvalifikovat rodinu a model procesoru, aby bylo zajištěno, že instrukce SYSENTER/SYSEXIT jsou skutečně přítomny
Syscall
Poslední možnost, syscall
instrukce, do značné míry umožňuje stejnou funkčnost jako sysenter
návod. Existence obou je způsobena tím, že jeden (systenter
) byl představen Intelem, zatímco druhý (syscall
) byla představena společností AMD.
Specifické pro Linux
V linuxovém jádře lze pro realizaci systémového volání zvolit kteroukoli ze tří výše uvedených možností.
Viz také Definitivní průvodce systémovými voláními systému Linux .
Jak již bylo uvedeno výše, int 0x80
metoda je jediná ze 3 vybraných implementací, která může běžet na jakémkoli CPU i386, takže je to jediná, která je vždy dostupná pro 32bitový uživatelský prostor.
(syscall
je jediný, který je vždy dostupný pro 64bitový uživatelský prostor, a jediný, který byste kdy měli používat v 64bitovém kódu; Jádra x86-64 lze sestavit bez CONFIG_IA32_EMULATION
a int 0x80
stále vyvolává 32bitové ABI, které zkrátí ukazatele na 32bitové.)
Aby bylo možné přepínat mezi všemi 3 možnostmi, je každému spuštěnému procesu udělen přístup ke speciálnímu sdílenému objektu, který umožňuje přístup k implementaci systémového volání zvolené pro běžící systém. Toto je podivně vypadající linux-gate.so.1
jste již mohli narazit na nevyřešenou knihovnu při použití ldd
nebo podobně.
(arch/x86/vdso/vdso32-setup.c)
if (vdso32_syscall()) {
vsyscall = &vdso32_syscall_start;
vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;
} else if (vdso32_sysenter()){
vsyscall = &vdso32_sysenter_start;
vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;
} else {
vsyscall = &vdso32_int80_start;
vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;
}
Abyste to mohli využít, stačí načíst všechna vaše registrační čísla systémového volání v eax, parametry v ebx, ecx, edx, esi, edi jako u int 0x80
implementace systémového volání a call
hlavní rutina.
Bohužel to není tak snadné; aby se minimalizovalo bezpečnostní riziko pevné předdefinované adresy, umístění, na kterém vdso
(virtuální dynamický sdílený objekt ) bude viditelný v procesu je náhodný, takže budete muset nejprve zjistit správné umístění.
Tato adresa je individuální pro každý proces a je předána procesu, jakmile je spuštěn.
V případě, že jste to nevěděli, při spuštění v Linuxu každý proces dostane ukazatele na parametry předané po svém spuštění a ukazatele na popis proměnných prostředí, pod kterými běží, předané na jeho zásobníku - každá z nich je ukončena NULL.
Kromě toho je předán třetí blok tzv. elfích pomocných vektorů, které následují po výše zmíněných. Správné umístění je zakódováno v jednom z nich, který nese typový identifikátor AT_SYSINFO
.
Rozložení zásobníku tedy vypadá takto (adresy rostou směrem dolů):
- parametr-0
- ...
- parametr-m
- NULL
- životní prostředí-0
- ....
- životní prostředí-n
- NULL
- ...
- Vektor pomocných elfů:
AT_SYSINFO
- ...
- Vektor pomocných elfů:
AT_NULL
Příklad použití
Chcete-li najít správnou adresu, budete muset nejprve přeskočit všechny argumenty a všechny ukazatele prostředí a poté začít hledat AT_SYSINFO
jak je znázorněno v příkladu níže:
#include <stdio.h>
#include <elf.h>
void putc_1 (char c) {
__asm__ ("movl $0x04, %%eax\n"
"movl $0x01, %%ebx\n"
"movl $0x01, %%edx\n"
"int $0x80"
:: "c" (&c)
: "eax", "ebx", "edx");
}
void putc_2 (char c, void *addr) {
__asm__ ("movl $0x04, %%eax\n"
"movl $0x01, %%ebx\n"
"movl $0x01, %%edx\n"
"call *%%esi"
:: "c" (&c), "S" (addr)
: "eax", "ebx", "edx");
}
int main (int argc, char *argv[]) {
/* using int 0x80 */
putc_1 ('1');
/* rather nasty search for jump address */
argv += argc + 1; /* skip args */
while (*argv != NULL) /* skip env */
++argv;
Elf32_auxv_t *aux = (Elf32_auxv_t*) ++argv; /* aux vector start */
while (aux->a_type != AT_SYSINFO) {
if (aux->a_type == AT_NULL)
return 1;
++aux;
}
putc_2 ('2', (void*) aux->a_un.a_val);
return 0;
}
Jak uvidíte, když se podíváte na následující úryvek /usr/include/asm/unistd_32.h
v mém systému:
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
Systémové volání, které jsem použil, je číslo očíslované 4 (zápis), jak je předáno v registru eax. Vezmeme-li deskriptor souboru (ebx =1), ukazatel dat (ecx =&c) a velikost (edx =1) jako argumenty, každý předaný v odpovídající registr.
Abych to zkrátil
Porovnání údajně pomalu běžícího int 0x80
systémové volání na jakékoli CPU Intel s (doufejme) mnohem rychlejší implementací pomocí (skutečně vynalezeného AMD) syscall
instrukce srovnává jablka s pomeranči.
IMHO:S největší pravděpodobností sysenter
instrukce namísto int 0x80
by měl být testován zde.
Když voláte jádro (systémové volání), musí se stát tři věci:
- Systém přejde z „uživatelského režimu“ do „režimu jádra“ (kruh 0).
- Zásobník se přepne z „uživatelského režimu“ do „režimu jádra“.
- Je proveden skok na vhodnou část jádra.
Jakmile je jádro uvnitř, kód jádra bude muset vědět, co vlastně chcete, aby jádro dělalo, a proto vloží něco do EAX a často více věcí do jiných registrů, protože existují věci jako „název souboru, který chcete otevřít " nebo "vyrovnávací paměť pro čtení dat ze souboru do" atd. atd.
Různé procesory mají různé způsoby, jak dosáhnout výše uvedených tří kroků. V x86 existuje několik možností, ale dvě nejoblíbenější pro ručně psaný asm jsou int 0xnn
(32bitový režim) nebo syscall
(64bitový režim). (K dispozici je také 32bitový režim sysenter
, představený společností Intel ze stejného důvodu, proč AMD představilo verzi syscall
v 32bitovém režimu :jako rychlejší alternativa k pomalému int 0x80
. 32bitový glibc používá jakýkoli účinný dostupný mechanismus systémového volání, pouze pomocí pomalého int 0x80
pokud není k dispozici nic lepšího.)
64bitová verze syscall
instrukce byla zavedena s architekturou x86-64 jako rychlejší způsob zadávání systémového volání. Má sadu registrů (pomocí mechanismů x86 MSR), které obsahují adresu RIP, na kterou chceme přeskočit, jaké hodnoty selektoru načíst do CS a SS a pro provedení přechodu Ring3 na Ring0. Ukládá také zpáteční adresu do ECX/RCX. [Prosím, přečtěte si návod k sadě s pokyny pro všechny podrobnosti tohoto návodu – není to úplně triviální!]. Protože procesor ví, že se to přepne na Ring0, může přímo dělat správnou věc.
Jedním z klíčových bodů je syscall
pouze manipuluje s registry; neprovádí žádné načítání ani ukládání. (Proto přepíše RCX uloženým RIPem a R11 uloženým RFLAGS). Přístup k paměti závisí na tabulkách stránek a položky tabulky stránek mají bit, díky kterému jsou platné pouze pro jádro, nikoli pro uživatelský prostor, takže přístup k paměti zatímco změna úrovně oprávnění může vyžadovat čekání oproti pouhému zápisu registrů. V režimu jádra bude jádro normálně používat swapgs
nebo nějaký jiný způsob, jak najít zásobník jádra. (syscall
není modifikovat RSP; stále ukazuje na zásobník uživatelů při vstupu do jádra.)
Při návratu pomocí instrukce SYSRET se hodnoty obnovují z předem určených hodnot v registrech, takže opět je to rychlé, protože procesor stačí nastavit pár registrů. Procesor ví, že se změní z Ring0 na Ring3, takže může rychle dělat správné věci.
(CPU AMD podporují syscall
instrukce z 32bitového uživatelského prostoru; CPU Intel ne. x86-64 byl původně AMD64; to je důvod, proč máme syscall
v 64bitovém režimu. AMD přepracovalo jádro syscall
pro 64bitový režim, tedy 64bitový syscall
vstupní bod jádra se výrazně liší od 32bitového syscall
vstupní bod v 64bitových jádrech.)
int 0x80
varianta použitá v 32bitovém režimu rozhodne, co dělat, na základě hodnoty v tabulce deskriptorů přerušení, což znamená čtení z paměti. Zde najde nové hodnoty CS a EIP/RIP. Nový registr CS určuje novou úroveň "zvonění" - v tomto případě Ring0. Poté použije novou hodnotu CS k nahlédnutí do segmentu stavu úloh (na základě registru TR), aby zjistil, který ukazatel zásobníku (ESP/RSP a SS), a nakonec skočí na novou adresu. Protože se jedná o méně přímé a obecnější řešení, je také pomalejší. Staré EIP/RIP a CS jsou uloženy v novém zásobníku spolu se starými hodnotami SS a ESP/RSP.
Při návratu pomocí instrukce IRET procesor přečte návratovou adresu a hodnoty ukazatele zásobníku ze zásobníku a načte ze zásobníku také nový segment zásobníku a hodnoty segmentu kódu. Tento proces je opět obecný a vyžaduje poměrně málo čtení paměti. Protože je to generické, procesor bude muset také zkontrolovat "měníme režim z Ring0 na Ring3, pokud ano, změňte tyto věci".
Stručně řečeno, je to rychlejší, protože to tak mělo fungovat.
Pro 32bitový kód ano, určitě můžete použít pomalý a kompatibilní int 0x80
pokud chcete.
Pro 64bitový kód int 0x80
je pomalejší než syscall
a zkrátí vaše ukazatele na 32bitové, takže jej nepoužívejte. Viz Co se stane, když použijete 32bitové int 0x80 Linux ABI v 64bitovém kódu? Navíc int 0x80
není k dispozici v 64bitovém režimu na všech jádrech, takže není bezpečný ani pro sys_exit
který nebere žádné ukazatele:CONFIG_IA32_EMULATION
lze deaktivovat, a zejména je zakázáno v subsystému Windows pro Linux.