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

Mysli slobodne. Uč sa slobodne. — Zo slobodnej knižnice Wikibooks ~ Wikiknihy.
Smazaný obsah Přidaný obsah
Fabcde (diskusia | príspevky)
dBez shrnutí editace
Bez shrnutí editace
Riadok 2: Riadok 2:


== Stručný prehľad ==
== Stručný prehľad ==

=== Syntax a kompilácia ===
=== Syntax a kompilácia ===
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. Jedným z rozdielov týchto dvoch syntaxí je opačné poradie argumentov v niektorých inštrukciách.<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"]
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. Jedným z rozdielov týchto dvoch syntaxí je opačné poradie argumentov v niektorých inštrukciách.<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"]
Riadok 269: Riadok 268:
* http://frdsa.fri.uniza.sk/~janosik/Kniha/ProgJSA.html
* http://frdsa.fri.uniza.sk/~janosik/Kniha/ProgJSA.html
* https://www.pcrevue.sk/a/ASSEMBLER-pod-Windows--Uvod--1--cast
* https://www.pcrevue.sk/a/ASSEMBLER-pod-Windows--Uvod--1--cast

== Pozri aj ==
* [[Vývoj operačného systému]]

[[Kategória:Počítače]]
[[Kategória:Počítače]]

Verzia z 13:08, 30. august 2017

Toto sú poznámky k niekoľkým jednoduchým programom napísaných v jazyku symbolických adries (assembly language) určených pre platformu x86-64Windows x64. Väčšinou sú uvedené dve verzie programov, prvá pre prekladač (kompilátor) Netwide Assembler (NASM), druhá pre GNU Assembler (GAS).

Stručný prehľad

Syntax a kompilácia

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. Jedným z rozdielov týchto dvoch syntaxí je opačné poradie argumentov v niektorých inštrukciách.[1] Napríklad inštrukcia "vlož hodnotu nula do registra AX" sa v NASM zapisuje MOV AX, 0, v GAS MOV $0, %AX. Intelovská syntax (NASM) pripomína priraďovací príkaz vyšších programovacích jazykov AX=0, AT&T syntax (GAS) skôr niečo ako 0->AX.

Objektové súbory sú zlinkované do spustiteľného exe súboru pomocou linkeru golink, alebo ld. Kompilátor as (GNU Assembler) a linker ld sú súčasťou gcc. Kvôli jednoduchšiemu rozlíšeniu majú zdrojové súbory programov určených pre kompilátor NASM príponu .asm, objektové .obj, pre GNU Assembler .s, resp .o.

Volanie WinAPI

Programy bežiace v Reálnom móde (operačný systém MS-DOS) alebo v móde (režime) Virtual 8086 (operačný systém Windows) mohli využívať služby operačného systému MS-DOS (MS-DOS API). Tieto sa volali pomocou softvérového prerušenia inštrukciou INT, napríklad INT 21h. 64-bitové verzie OS Windows režim Virtual 8086 nepodporujú. Služby operačného systému je preto možné zabezpečiť jedine volaním funkcií 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 (volacia konvencia).

Toto sú niektoré z najčastejšie používaných volacích konvencií (calling convention) v prostredí MS Windows:

  • cdecl - C declaration, pochádza z jazyka C, parametre sú ukladané na vrchol zásobníka postupne sprava doľava (kvôli podpore premenlivého počtu argumentov), výsledok je uložený buď v registry EAX (integer) alebo ST0 (float), zásobník čistí volajúca funkcia
  • pascal - parametre sú ukladané na vrchol zásobníka zľava doprava, zásobník čistí volaná funkcia (napríklad inštrukciou RET n)
  • stdcall - štandard pre Win32 API, parametre sú ukladané na vrchol zásobníka sprava doľava (ako cdecl), ale zásobník čistí volaná funkcia (ako pascal)
  • Microsoft x64 - volania WinAPI v 64-bitových programoch pre MS Windows, prvé štyri parametre sú uložené v RCX/XMM0, RDX/XMM1, R8/XMM2, R9/XMM3 (integer/float), zvyšné v zásobníku sprava doľava, výsledok je vrátený v registry RAX alebo XMM0, zásobník čistí volajúca funkcia

Hello, World!

Náš prvý program vypíše v príkazovom riadku krátky text a skončí. Verzia pre NASM:

; HelloWorld.asm

; kompilacia:
;   nasm -f win64 HelloWorld.asm
; linkovanie:
;   golink /console /ni /entry main HelloWorld.obj kernel32.dll
; alternativne linkovanie:
;   ld -e main -s HelloWorld.obj -o HelloWorld.exe c:\windows\system32\kernel32.dll


global main

extern GetStdHandle
extern WriteFile
extern ExitProcess


        section .bss use64             ; neinicializovana datova oblast
lpNumberOfBytesWritten:  resd      1


        section .text use64            ; Program code
message:                 db      "Hello, World!",0xd,0xa
MESSAGE_LEN:             equ     $-message

main:
        ; rax = GetStdHandle(-11)
        ; HANDLE hStdHandle = WINAPI GetStdHandle (_In_ DWORD nStdHandle)
        ; nStdHandle: STD_INPUT_HANDLE=-10 , STD_OUTPUT_HANDLE=-11 , STD_ERROR_HANDLE=-12
        mov ecx, -11                   ; 1. param _In_ DWORD nStdHandle
        call GetStdHandle

        ; rax = WriteFile(%rax, $message, $MESSAGE_LEN, %rsp-4, 0)
        ; BOOL bErrorFlag = WINAPI WriteFile (_In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped)
        ; WriteConsole(handle, &msg[0], 13, &written, 0)
        mov rcx, rax                   ; 1. param _In_ HANDLE hFile
        mov rdx, qword message         ; 2. param _In_ LPCVOID lpBuffer
        mov r8d, dword MESSAGE_LEN     ; 3. param _In_ DWORD nNumberOfBytesToWrite
        mov r9, lpNumberOfBytesWritten ; 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten
        push qword 0                   ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped
        call WriteFile
        add rsp, 8                     ; uvolnenie miesta v zasobniku

        ; ExitProcess(0)
        ; VOID WINAPI ExitProcess( _In_ UINT uExitCode)
        xor ecx, ecx                   ; UINT je 32 bit aj v 64 bitovom prostredi
        call ExitProcess

Program síce nealokuje miesto v zásobníku, ako to vyžaduje volacia konvencia Microsoft x64 (podrobnosti ďalej), napriek tomu sa dal zostaviť aj spustiť. Kompilácia:

G:\>nasm -f win64 HelloWorld.asm

Linkovanie:

G:\>golink /console /ni /entry main HelloWorld.obj kernel32.dll

alebo:

G:\>ld -e main -s HelloWorld.obj -o HelloWorld.exe c:\windows\system32\kernel32.dll

Ak kompilácia a linkovanie prebehli úspešne, môžme vyskúšať náš prvý 64-bitový program:

G:\>dir
15.07.2017  13:27             2 193 HelloWorld.asm
15.07.2017  13:27             1 536 HelloWorld.exe
15.07.2017  13:27               548 HelloWorld.obj

G:\>HelloWorld.exe
Hello, World!

Direktíva global main deklaruje návestie main ako globálne a linker ho môže použiť ako štartovaciu adresu programu.

Direktíva extern GetStdHandle 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 section .bss use64 definuje oblasť neinicializovaných dát. V nej je vyhradený 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 section .text use64 uvádza nasledujúci segment ako programový.

Na adrese mesage je uložený text pozdravu. Pristupuje sa k nej síce ako k premennej, nachádza sa však v sekcii programu, preto je readonly.

Konštanta MESSAGE_LEN obsahuje počet bajtov (dĺžku textu), ktorý chceme vypísať. Konštanta, na rozdiel od premennej, nie je uložená na nejakej konkrétnej adrese, ale v čase kompilácie bude každý odkaz na ňu, napríklad mov r8d, dword MESSAGE_LEN nahradený jej skutočnou hodnotou mov r8d, 15 (podobne ako #define MESSAGE_LEN 15 v jazyku C).

Inštrukcie

        mov ecx, -11
        call GetStdHandle

naplnia register ECX hodnotou -11 (STD_OUTPUT_HANDLE) a zavolajú funkciu GetStdHandle.

Funkcia vráti v registry RAX (v súlade s volacou konvenciou) handle zariadenia STDOUT.

Inštrukcie

        mov rcx, rax                   ; 1. param _In_ HANDLE hFile
        mov rdx, qword message         ; 2. param _In_ LPCVOID lpBuffer
        mov r8d, dword MESSAGE_LEN     ; 3. param _In_ DWORD nNumberOfBytesToWrite
        mov r9, lpNumberOfBytesWritten ; 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten
        push qword 0                   ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped
        call WriteFile

vložia prvé štyri argumenty funkcie WriteFile do príslušných registrov, piaty do zásobníka a zavolajú ju.

Ďalšia inštrukcia add rsp, 8 uvoľní miesto v zásobníku obsadené inštrukciou push qword 0.

Nakoniec

        xor ecx, ecx
        call ExitProcess

vynuluje obsah registra ECX a ukončí program. Jediným argumentom funkcie ExitProcess (uložený v registry ECX) je exit code programu. Verzia pre GNU assembler:

# HelloWorld.s

# kompilacia:
#   as HelloWorld.s -o HelloWorld.o
# linkovanie:
#   ld -e main -s HelloWorld.o -o HelloWorld.exe c:\windows\system32\kernel32.dll
# alternativna kompilacia+linkovanie:
#   gcc -m64 -nostartfiles -Wl,-emain -o HelloWorld.exe HelloWorld.s c:\windows\system32\kernel32.dll

        .global main

        .section .bss
lpNumberOfBytesWritten:   .space      4

        .section .text
message:                  .ascii  "Hello, World!\n"
MESSAGE_LEN = . - message

main:
        /* rax = GetStdHandle(-11) */
        /* HANDLE hStdHandle = WINAPI GetStdHandle (_In_ DWORD nStdHandle) */
        /* nStdHandle: STD_INPUT_HANDLE=-10 , STD_OUTPUT_HANDLE=-11 , STD_ERROR_HANDLE=-12 */
        mov $-11, %ecx                   # 1. param _In_ DWORD nStdHandle
        call GetStdHandle

        /* rax = WriteFile(%rax, $message, $MESSAGE_LEN, %rsp-4, 0) */
        /* BOOL bErrorFlag = WINAPI WriteFile (_In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped) */
        /* WriteConsole(handle, &msg[0], 13, &written, 0) */
        mov %rax, %rcx                   # 1. param _In_ HANDLE hFile
        mov $message, %rdx               # 2. param _In_ LPCVOID lpBuffer
        mov $MESSAGE_LEN, %r8d           # 3. param _In_ DWORD nNumberOfBytesToWrite
        mov $lpNumberOfBytesWritten, %r9 # 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten
        pushq $0                         # 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped
        call WriteFile
        add $8, %rsp                     # uvolnenie miesta v zasobniku

        /* 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

Kompilácia:

G:\>as HelloWorld.s -o HelloWorld.o

Linkovanie:

G:\>ld -e main -s HelloWorld.o -o HelloWorld.exe c:\windows\system32\kernel32.dll

alebo:

G:\>gcc -m64 -nostartfiles -Wl,-s,-emain -o HelloWorld.exe HelloWorld.s c:\windows\system32\kernel32.dll

Výsledok:

G:\>dir
15.07.2017  15:32             2 198 HelloWorld.s
16.07.2017  12:48               584 HelloWorld.o
16.07.2017  12:48             1 536 HelloWorld.exe

G:\>HelloWorld.exe
Hello, World!

Zásobník

Zásobník (stack) je pamäťová štruktúra typu LIFO. Hardvérový zásobník je realizovaný priamo v pamäti počítača. V ďalšom texte sa pod zásobníkom rozumie vždy hardvérový zásobník, nie nejaká jeho softvérová implementácia.

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 aj dočasných, pomocných hodnôt. Používa sa 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. (RSP 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í

SUB RSP, 8
MOV [RSP], RAX

Keďže architektúra x86/x86-64 používa na ukladanie viac-bajtových hodnôt usporiadanie little-endian, t.j. na nižšej adrese je uložený menej významný/nižší bajt, v zásobníku bude RAX uložený:

|              |
+==============+
|   0. bajt    |    <- RSP (Vrchol zásobníka) po
+--------------+
|   1. bajt    |    ^
+--------------+    |
|   2. bajt    |    |
+--------------+    |
|   3. bajt    |    |
+--------------+    |
|   4. bajt    |    |  RSP - 8
+--------------+    |
|   5. bajt    |    |
+--------------+    |
|   6. bajt    |    |
+--------------+    |
|   7. bajt    |    |
+==============+
|      .       |    <- RSP (Vrchol zásobníka) pred
+--------------+
|      .       |
+--------------+
|      .       |
+==============+    <- Dno zásobníka

Z vrcholu zásobníka sa hodnoty vyberajú inštrukciou POP. Inštrukcia POP RAX 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:

MOV RAX, [RSP]
ADD RSP, 8

Ku hodnotám uloženým v zásobníku je možné stále pristupovať aj priamo, ako ku hocijakým iným dátam uloženým kdekoľvek v pamäti, napríklad pomocou relatívneho odkazu na vrchol zásobníka:

MOV EAX, [RSP+4]

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:

SUB RSP, n

Nepotrebné miesto na vrchole zásobníka uvoľní:

ADD RSP, n

Volacia konvencia Microsoft x64

Volanie funkcie (podprogramu) sa realizuje inštrukciou CALL. Inštrukcia CALL najskôr uloží 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 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 preto 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.

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.

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) je zodpovedná volaná funkcia.

Funkcia vracia celočíselný výsledok v registri RAX, desatinný v XMM0.

| lokálne premenné a  |
| volatile registre   |
+---------------------+
| návratová adresa    |   CALL
| z podprogramu (RIP) |
+---------------------+ 
| 32. bajtov    (RCX) |    \
| shadow space  (RDX) |    |
|               (R8)  |    |
|               (R9)  |    |
+---------------------+    |
| 5. argument         |    |
+---------------------+    |
| 6. argument         |     > zodpovednosť volajúceho za alokovanie a uvoľnenie
+---------------------+    |
|          .          |    |
+---------------------+    |
| posledný argument   |    |
+---------------------+    |
| lokálne premenné a  |    |
| volatile registre   |    /
+---------------------+

Referencie

  1. Ram Narayan (2007-10-17). "Linux assemblers: A comparison of GAS and NASM"

Ďalšie zdroje

Pozri aj