GNU/Linux >> Znalost Linux >  >> Linux

Systémové volání Intel x86 vs x64

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í:

  1. Dočasně (interně) uloží aktuální obsah registrů SS, ESP, EFLAGS, CS a> EIP.

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

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

  4. Vloží chybový kód do nového zásobníku (pokud je to vhodné).

  5. Načte selektor segmentu pro nový kódový segment a ukazatel nové instrukce (z hradla přerušení) do registrů CS a EIP.

  6. Pokud je volání přes bránu přerušení, vymaže příznak IF v registru EFLAGS.

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

  1. Provede kontrolu oprávnění.

  2. Obnoví registry CS a EIP na jejich hodnoty před přerušením nebo výjimkou.

  3. Obnoví registr EFLAGS.

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

  5. 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:

  1. Systém přejde z „uživatelského režimu“ do „režimu jádra“ (kruh 0).
  2. Zásobník se přepne z „uživatelského režimu“ do „režimu jádra“.
  3. 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.


Linux
  1. Jak zjistit, zda systém podporuje Intel Amt?

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

  3. vytisknout zásobník volání v C nebo C++

  1. Kde najdu zdrojový kód systémového volání?

  2. Kontrola, zda errno !=EINTR:co to znamená?

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

  1. Linux – metody vyvolání systémového volání v novém jádru?

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

  3. Proč se telefonní čísla systému Linux v x86 a x86_64 liší?