Programovanie v assembleri vo Windows x64 (x86-64): Rozdiel medzi revíziami

Smazaný obsah Přidaný obsah
Fabcde (diskusia | príspevky)
formulacie, HelloWorld v.2
Fabcde (diskusia | príspevky)
d formulacie
Riadok 3:
== Stručný prehľad ==
=== Syntax a kompilácia ===
VäčšinouVäčšina programov tuv uvedenétomto dvedokumente verzieje programovuvedená v dvoch verziách - prvá pre [[w:Prekladač|prekladač]] (kompilátor) [[w:Netwide_Assembler|Netwide Assembler]] (NASM), druhá pre [https://cs.wikipedia.org/wiki/GNU_Assembler GNU Assembler] (GAS). Kým NASM používa syntax Intelu, dominujúcu v prostredí MS-DOS a Windows, GNU Asembler používa syntax AT&T, prevládajúcu v Unixovom svete. Asi najvýraznejsímnajmätúcejším rozdielom týchto dvoch syntaxí je prehodené poradie argumentov niektorých inštrukcií.<ref>Ram Narayan (2007-10-17). [https://www.ibm.com/developerworks/library/l-gas-nasm/index.html "Linux assemblers: A comparison of GAS and NASM"]
</ref> Napríklad inštrukcia "vlož hodnotu nula do registra AX" sa v NASM zapisuje <code>MOV AX, 0</code>, v GAS <code>MOV $0, %AX</code>. (Intelovská syntax pripomína priraďovací príkaz vyšších programovacích jazykov <code>AX = 0</code>, syntax AT&T skôr niečo ako <code>0 -> AX</code>.)
 
Riadok 9:
 
=== Volanie WinAPI ===
Programy bežiace v [https://cs.wikipedia.org/wiki/Re%C3%A1ln%C3%BD_re%C5%BEim Reálnom móde] (operačný systém MS-DOS) alebo v móde (režime) [https://cs.wikipedia.org/wiki/Virtual_8086_mode Virtual 8086] (operačný systém Windows) mohli využívať služby operačného systému MS-DOS (prostredníctvom [[wikipedia:MS-DOS_API|MS-DOS API)]]. Tieto sa volali pomocou [https://cs.wikipedia.org/wiki/P%C5%99eru%C5%A1en%C3%AD#Softwarov.C3.A9_p.C5.99eru.C5.A1en.C3.AD softvérového prerušenia] inštrukciou INT, napríklad <code>INT 21h</code><ref>[http://www.cs.cmu.edu/~ralf/files.html Ralf Brown's Interrupt List]</ref>. Keďže 64-bitové verzie OS Windows režim Virtual 8086 nepodporujú., Službyslužby operačného systému je preto možné zabezpečiť jedine volaním funkcií [https://cs.wikipedia.org/wiki/Windows_API Windows API] (WinAPI). Funkcii (podprogramu) je zvyčajne potrebné nejakým spôsobom odovzdať argumenty a opačným smerom zase výsledok. V zásade nie sú žiadne obmedzenia týkajúce sa spôsobu odovzdávania údajov medzi volajúcim a volaným podprogramom. Je možné zvoliť akýkoľvek fungujúci spôsob, či už pomocou registrov, pamäti, zásobníka, atď, len treba o každej volanej funkcii (podprograme) vedieť, kde očakáva argumenty a kam ukladá výsledok ([https://cs.wikipedia.org/wiki/Volac%C3%AD_konvence volacia konvencia]).
 
Toto sú niektoré z najčastejšie používaných [[wikipedia:X86_calling_conventions|volacích konvencií (calling convention)]] v prostredí MS Windows:
Riadok 19:
 
=== Hello, World! ===
Náš prvý program vypíše v príkazovom riadku krátky text a skončí.

Verzia pre NASM:<syntaxhighlight lang="nasm" line="1">
; HelloWorld.asm
 
Řádek 65 ⟶ 67:
; ExitProcess(0)
; VOID WINAPI ExitProcess( _In_ UINT uExitCode)
xor ecx, ecx ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
call ExitProcess
</syntaxhighlight>Program síce nealokuje miesto v zásobníku (shadow space, podrobnosti ďalej) tak ako to vyžaduje volacia konvencia Microsoft x64 (podrobnosti ďalej), napriek tomu sa dal zostaviť aj spustiť.
 
Kompilácia:<syntaxhighlight>
Řádek 94 ⟶ 96:
Direktíva <code>extern GetStdHandle</code> deklaruje symbol GetStdHandle (WinAPI funkcia) ako externý, čiže nachádzajúci sa v niektorom z ďalších pripojených súborov, v tomto prípade v dynamicky linkovanej knižnici kernel32.dll. (Napriek tej 32 v názve, vo Windows x64 je to 64-bitová knižnica.)
 
Direktíva <code>section .bss use64</code> definuje oblasť neinicializovaných dát. V nej je vyhradenývymedzený priestor 4 bajty pre uloženie počtu úspešne vypísaných bajtov textu pozdravu. Program túto hodnotu ďalej nepoužíva.
 
Direktíva <code>section .text use64</code> uvádza nasledujúci segment ako programový.
Řádek 121 ⟶ 123:
 
Nakoniec<syntaxhighlight lang="nasm" line="1" start="46">
xor ecx, ecx ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
xor ecx, ecx
call ExitProcess
</syntaxhighlight>vynuluje obsah registra ECX a ukončí program. Jediným argumentom funkcie ExitProcess (uložený v registry ECX) je exit code programu.
Řádek 184 ⟶ 186:
 
=== Zásobník ===
[https://cs.wikipedia.org/wiki/Z%C3%A1sobn%C3%ADk_%28datov%C3%A1_struktura%29 Zásobník] (stack) je pamäťová štruktúra typu LIFO. Hardvérový zásobník je realizovaný priamo v operačnej pamäti počítača. V ďalšom texte sazásobníkom pod zásobníkombudeme rozumierozumieť vždy hardvérový zásobník, nie nejakánejakú jeho softvérovásoftvérovú implementáciaimplementáciu.
 
Zásobník je možné používať ľubovoľne podľa potreby. Bežne sa používa na ukladanie lokálnych premenných, a ajalebo dočasných, pomocných hodnôt. PoužívaČasto sa používa aj na odovzdávanie argumentov podprogramu. Inštrukcia CALL na vrchol zásobníka tiež ukladá návratovú adresu z podprogramu.
 
V architektúrach x86 a x86-64 sa ku zásobníku pristupuje pomocou inštrukcií PUSH a POP. Zásobník rastie (plní sa) smerom od vyššej adresy k nižšej. Na jeho vrchol ukazuje register RSP. (Stack Pointer). RSP tak obsahuje najnižšiu adresu, na ktorej je niečo uložené).
 
Inštrukcia PUSH vloží novú hodnotu na vrchol zásobníka tak, že zmenší hodnotu registra RSP o počet vložených bajtov, a na túto adresu uloží novú hodnotu. Napríklad inštrukciu PUSH RAX si môžme predstaviť ako dvojicu inštrukcií<syntaxhighlight lang="nasm">
Řádek 196 ⟶ 198:
| |
+==============+
| 0. bajt | <- RSP (Vrchol zásobníka) po ... nižšia adresa
+--------------+
| 1. bajt | ^
Řádek 217 ⟶ 219:
+--------------+
| . |
+==============+ <- Dno zásobníka ... vyššia adresa
</syntaxhighlight>Z vrcholu zásobníka sa hodnoty vyberajú inštrukciou POP. Inštrukcia <code>POP RAX</code> z vrcholu zásobníka prečíta hodnotu, vloží ju do registra RAX, a následne uvoľní miesto v zásobníku, podobne ako:<syntaxhighlight lang="nasm">
MOV RAX, [RSP]
ADD RSP, 8
</syntaxhighlight>Ku hodnotám uloženým v zásobníku je možné stále možné pristupovať aj priamo, ako ku hocijakým iným dátam uloženým kdekoľvek v operačnej pamäti, napríklad pomocou relatívneho odkazu na vrchol zásobníka:<syntaxhighlight lang="nasm">
MOV EAX, [RSP+4]
</syntaxhighlight>Pamäť zásobníka mimo rozsahu vymedzenom registrom RSP je nestála (volatile) a môže ju prepísať OS alebo debuger. Pre bezpečné uloženie údajov do zásobníka je preto nutné vždy najskôr alokovať potrebný priestor:<syntaxhighlight lang="nasm">
Řádek 228 ⟶ 230:
ADD RSP, n
</syntaxhighlight>Poznámka: Rozhranie X86-64 ABI použité v System V umožňuje používať aj [[wikipedia:Red_zone_(computing)|Red zone]] - oblasť 128 bajtov tesne nad vrcholom zásobníka.
 
=== Podprogram ===
Volanie funkcie (podprogramu) sa v jazyku symbolických adries realizuje inštrukciou CALL. Inštrukcia CALL najskôr vloží na vrchol zásobníka hodnotu registra RIP ([https://cs.wikipedia.org/wiki/%C4%8C%C3%ADta%C4%8D_instrukc%C3%AD Instruction Pointer], niekedy nazývaný aj PC - Program Counter), v ktorom sa už nachádza adresa nasledujúcejďalšej inštrukcie. Následne zmenou hodnoty registra RIP sa zrealizuje skok na požadovanú adresu. Podprogram končí inštrukciou RET, ktorá vyberieodoberie z vrcholu zásobníka návratovú adresu (pôvodnú hodnotu registra RIP) a vloží ju späť do registra RIP. Program potom pokračuje tam, kde bol prerušený podprogramom, čiže inštrukciou bezprostredne nasledujúcou za inštrukciou CALL.
 
=== Volacia konvencia Microsoft x64 ===
64-bitové verzie funkcií WindowsAPI používajú volaciu konvenciu Microsoft x64.
Volanie funkcie (podprogramu) sa realizuje inštrukciou CALL. Inštrukcia CALL najskôr vloží na vrchol zásobníka hodnotu registra RIP, v ktorom sa už nachádza adresa nasledujúcej inštrukcie. Následne zmenou hodnoty registra RIP sa zrealizuje skok na požadovanú adresu. Podprogram končí inštrukciou RET, ktorá vyberie z vrcholu zásobníka návratovú adresu (pôvodnú hodnotu registra RIP) a vloží ju späť do registra RIP.
 
64-bitové verzie funkcií WindowsAPI používajú volaciu konvenciu Microsoft x64. Prvé štyri argumenty sú uložené v registroch (64-bitová architektúra x86-64 má oproti 32-bitovej architektúre x86 k dispozícii viac registrov). V prípade celočíselných hodnôt, (vrátane ukazovateľov), v RCX, RDX, R8 a R9 (v tomto poradí), v prípade argumentov s pohyblivou desatinnou čiarkou v XMM0, XMM1, XMM2, XMM3. Prvý argument je teda uložený buď v registry RCX alebo v XMM0, druhý v RDX alebo v XMM1, tretí v R8 alebo v XMM2, štvrtý v R9 alebo v XMM3. Parametre menšie než 64 bitov ignorujú vyššie bity, a pretonetreba ich netreba nulovať. Napríklad prvý parameter typu integer (aj v x86-64 je to 32 bitové celé číslo) stačí uložiť do ECX. Ďalšie argumenty sa ukladajú do zásobníka v poradí sprava doľava, rovnako ako pri stdcall.
 
Ďalšie argumenty sa ukladajú do zásobníka v poradí sprava doľava, rovnako ako pri stdcall.
Volacia konvencia Microsoft x64 vyžaduje, aby v zásobníku bolo alokovaných ďalších 32 bajtov, kam volaná API funkcia niekedy ukladá obsah registrov RCX, RDX, R8, R9. Tento 32 bajtový priestor (shadow space) musí volajúci alokovať vždy, a to aj v prípade, že funkcia má menej ako štyri parametre. Za uvoľnenie tohto miesta rovnako ako aj miesta pre ďalšie argumenty zodpovedá volajúci (na rozdiel od konvencie stdcall).
 
Non-leaf funkcia, čiže funkcia, ktorá tiež volá nejakú funkciu, vyžaduje zarovnanie zásobníka na 16 bajtov.
 
Volacia konvencia Microsoft x64 ďalej vyžaduje, aby v zásobníku bolo alokovaných ďalšíchdodatočných 32 bajtov, kam volaná API funkcia niekedy ukladá obsah registrov RCX, RDX, R8, R9. Tento 32 bajtový priestor (shadow space) musí volajúci alokovať vždy, a to aj v prípade, že funkcia má menej ako štyri parametre. Za uvoľnenie tohto miesta, rovnako ako aj miesta pre ďalšie argumenty, zodpovedá volajúci (na rozdiel od konvencie stdcall).
 
Registre RAX, RCX, RDX, R8, R9, R10, R11 sú považované za nestále (volatile) a volaná funkcia ich môže kedykoľvek trvalo zmeniť. Volajúci ich samozrejme môže po návrate z funkcie obnoviť, ak si predtým uschoval ich hodnoty. Naopak, registre RBX, RBP, RDI, RSI, RSP, R12, R13, R14 a R15 sú považované za stále (nonvolatile). Za ich obnovenie do pôvodného stavu (v prípade ich zmeny) zodpovedá volaná funkcia.
 
Funkcia vracia celočíselný výsledok v registri RAX, desatinný v XMM0.<syntaxhighlight>
 
| lokálne premenné a |
''Obsah zásobníka po zavolaní WinAPI funkcie :''<syntaxhighlight>
| volatile registre |
+---------------------+ ... nižšia adresa \
| lokálne premenné a | > volaná funkcia
| návratová adresa | CALL
| volatile registre | /
| z podprogramu (RIP) |
+=====================+
+---------------------+
| návratová adresa | CALL \
| 32. bajtov (RCX) | \
| z podprogramu (RIP) | |
| shadow space (RDX) | |
|+---------------------+ (R8) | |
| 32. bajtov (RCX) | \ (R9) | |
| shadow space (RDX) | | |
+---------------------+ |
| 5. argument (R8) | | |
| (R9) | | |
+---------------------+ |
+---------------------+ | 6. argument | > zodpovednosť volajúceho za alokovanie a uvoľnenie |
| 5. argument | | > volajúca funkcia
+---------------------+ |
+---------------------+ | . | |
| 6. argument | > zodpovednosť volajúceho |
+---------------------+ |
+---------------------+ | za alokovanie a uvoľnenie |
| posledný argument | |
| . | | |
+---------------------+ |
+---------------------+ | |
| lokálne premenné a | |
| posledný argument | | |
| volatile registre | /
+---------------------+ | |
| lokálne premenné a | | |
| volatile registre | / /
+=====================+ ... vyššia adresa
</syntaxhighlight>
 
=== Hello, World! v.2 ===
Program HelloWorld opravený v súlade s volacou konvenciou.
<syntaxhighlight lang="nasm" line="1">
 
Verzia pre NASM:<syntaxhighlight lang="nasm" line="1">
; HelloWorld.asm
 
Řádek 311 ⟶ 325:
; ExitProcess(0)
; VOID WINAPI ExitProcess( _In_ UINT uExitCode)
xor ecx, ecx ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
call ExitProcess
 
add rsp, 28h ; uvolnenie rezervovaneho miesta
 
</syntaxhighlight>Rozdiel v porovnaní s prvou verziou spočíva v rezervovaní miesta v zásobníku tak, aby sa sem vošli argumenty (8 bajtov pre piaty argument funkcie WriteFile) a shadow space (32 bajtov). Vďaka inštrukcii CALL, ktorá ešte na vrchol zásobníka vloží obsah RIP (8 bajtov) bude zásobník zarovnaný na požadovaných 16 bajtov:<syntaxhighlight lang="nasm" line="1" start="27">
sub rsp, 28h ; rezervovanie miesta v zasobniku pre 5-ty argument, shadow space a zarovnanie
</syntaxhighlight>Piaty argument potom samozrejme nie je možné vložiť na požadovanú pozíciu inštrukciou PUSH, ale:<syntaxhighlight lang="nasm" line="1" start="42">
mov qword [rsp+20h], 0 ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped
</syntaxhighlight>Zásobník je pripravený aj pre funkciu ExitProcess, stačí ju zavolať a až potom uvoľniť rezervované miesto:<syntaxhighlight lang="nasm" line="1" start="50">
add rsp, 28h ; uvolnenie rezervovaneho miesta
</syntaxhighlight>
 
== Referencie ==
<references />