Preskočiť na obsah

Programovanie v assembleri vo Windows x64 (x86-64)

Mysli slobodne. Uč sa slobodne. — Zo slobodnej knižnice Wikibooks ~ Wikiknihy.

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.

Stručný prehľad

[upraviť]

Syntax a kompilácia

[upraviť]

Väčšina programov v tomto dokumente je uvedená v dvoch verziách - prvá pre prekladač (kompilátor) Netwide Assembler (NASM), druhá pre GNU Assembler (GAS). 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 najmätúcejším rozdielom týchto dvoch syntaxí je prehodené poradie argumentov niektorých inštrukcií.[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 pripomína priraďovací príkaz vyšších programovacích jazykov AX = 0, syntax AT&T skôr niečo ako 0 -> AX.)

Skompilovaním zdrojového súboru príslušným kompilátorom (nasm resp. as) vznikne objektový súbor, ktorý je následne pomocou linkeru golink, resp. ld zlinkovaný do výsledného spustiteľného súboru. Kompilátor as (GNU Assembler) a linker ld sú súčasťou gcc. Kvôli jednoduchšiemu rozlíšeniu majú tu uvedené zdrojové súbory programov určených pre NASM príponu .asm, objektové .obj, a pre GNU Assembler príponu .s, resp. .o.

Volanie WinAPI

[upraviť]

Programy bežiace v Reálnom móde (operačný systém MS-DOS) prípadne aj v móde (režime) Virtual 8086 (operačný systém Windows) mohli využívať služby operačného systému MS-DOS prostredníctvom MS-DOS API. Tieto sa volali pomocou softvérového prerušenia inštrukciou INT, napríklad INT 21h[2]. Nakoľko 64-bitové verzie OS Windows režim Virtual 8086 nepodporujú, služby operačného systému je už možné zabezpečiť výlučne 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. Kvôli veľkému množstvu funkcií, ktoré sú k dispozícii (bežný prípad knižníc), bolo nutné zaviesť nejaké spoločné pravidlá - volacie konvencie (calling convention).

Toto sú niektoré z najčastejšie používaných volacích konvencií 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 registri 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 registri RAX alebo XMM0, zásobník čistí volajúca funkcia

Hello, World!

[upraviť]

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

Výpis 1a HelloWorld.asm (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 -o HelloWorld.exe HelloWorld.obj 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)
        mov ecx, 0                     ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
        call ExitProcess

Program síce nealokuje miesto v zásobníku (shadow space, podrobnosti ďalej) tak ako to vyžaduje volacia konvencia Microsoft x64, 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               551 HelloWorld.obj

G:\>HelloWorld.exe
Hello, World!

Obsah objektového aj spustiteľného súboru sa dá zobraziť programom objdump, napríklad:

G:\>objdump -fhD HelloWorld.obj

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, o čom sa môžeme presvedčiť ďalším užitočným programom 7-Zip:

G:\>"C:\Program Files\7-Zip\7z.exe" l c:\windows\system32\kernel32.dll

Direktíva section .bss use64 definuje oblasť neinicializovaných dát dostupných na čítanie aj zápis z ktorejkoľvek časti programu (neinicializovaná globálna premenná). V nej je vymedzený priestor 4 bajty pre uloženie počtu úspešne vypísaných bajtov textu pozdravu funkciou WriteFile. Program túto hodnotu už ďalej nepoužíva a určite by bolo vhodnejšie uložiť ju do zásobníka ako lokálnu premennú (pozri Hello, World! v.2).

Direktíva section .text use64 uvádza nasledujúci segment ako programový.

Na adrese message je uložený text pozdravu. Keďže sa nachádza v sekcii kódu, obsah tejto pamäťovej oblasti program môže čítať, ale pokus o jej modifikáciu vedie ku okamžitému ukončeniu programu. (Dá sa o tom jednoducho presvedčiť preusnutím premennej lpNumberOfBytesWritten do sekcii kódu. Program skončí chybou ERRORLEVEL=-1073741819 "Access violation.")

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 registri 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

        mov ecx, 0                     ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
        call ExitProcess

vynuluje obsah registra ECX a ukončí program. Jediným argumentom funkcie ExitProcess (uložený v registri ECX) je exit code programu.

Výpis 1b HelloWorld.s (Verzia pre GAS):

# HelloWorld.s

# kompilacia:
#   as -o HelloWorld.o HelloWorld.s
# linkovanie:
#   ld -e main -s -o HelloWorld.exe HelloWorld.o 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!\r\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)
        mov $0, %ecx                     /* 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi */
        call ExitProcess

V GAS je každý neznámy symbol považovaný za externý, preto nie je potrebné názvy API funkcií deklarovať direktívou extern. Nakoľko GAS assembler vo Windows nesprávne nahrádza escape sekvenciu pre nový riadok '\n' Unixovým LF (0x0a) namiesto správnej kombinácii CR+LF (0x0d,0x0a), bolo nutné hodnotu premennej message upraviť na "Hello, World!\r\n" (prípadne pomocou osmičkovej sústavy "Hello, World!\15\12").

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 200 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

[upraviť]

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 zásobníkom budeme rozumieť vždy hardvérový zásobník, nie nejakú jeho softvérovú implementá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, alebo dočasných, pomocných hodnôt. Často sa používa aj na odovzdávanie argumentov podprogramu. Inštrukcia CALL na vrchol zásobníka 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í

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 preto register RAX uložený:

|              |
+==============+
|   0. bajt    |    <- RSP = Vrchol zásobníka po (nižšia adresa)
+--------------+
|   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 (vyššia adresa)

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

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

Poznámka: Rozhranie X86-64 ABI použité v System V umožňuje používať aj Red zone - oblasť 128 bajtov tesne nad vrcholom zásobníka.

Podprogram

[upraviť]

Volanie funkcie (podprogramu) sa v jazyku symbolických adries realizuje inštrukciou CALL. Inštrukcia najskôr vloží na vrchol zásobníka hodnotu registra RIP (Instruction Pointer, niekedy nazývaný aj PC - Program Counter), v ktorom sa už nachádza adresa ďalšej inštrukcie. Následne zmenou hodnoty registra RIP sa zrealizuje skok na požadovanú adresu. Podprogram končí inštrukciou RET, ktorá odoberie 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

[upraviť]

64-bitové verzie funkcií WindowsAPI, rovnako ako aj funkcie knižníc GCC, MS Visual Studio (do 2013), Delphi, používajú volaciu konvenciu Microsoft x64[3].

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 registri 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, netreba ich 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.

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 dodatoč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.

Obsah zásobníka po zavolaní WinAPI funkcie :

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

Hello, World! v.2

[upraviť]

Program HelloWorld opravený v súlade s volacou konvenciou.

Výpis 2a HelloWorld.asm (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 -o HelloWorld.exe HelloWorld.obj c:\windows\system32\kernel32.dll


global main

extern GetStdHandle
extern WriteFile
extern ExitProcess


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

main:
        sub rsp, 38h                  ; rezervovanie miesta v zasobniku pre shadow space (32B), 5-ty argument funkcie WriteFile (8B), lokalnu premennu lpNumberOfBytesWritten (8B), zarovnanie (8B) ((instrukcia CALL vlozi do zasobnika este navratovu adresu, cize dalsich 8B))

        ; 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 (8B)
        mov rdx, qword message         ; 2. param _In_ LPCVOID lpBuffer (8B)
        mov r8d, dword MESSAGE_LEN     ; 3. param _In_ DWORD nNumberOfBytesToWrite (4B)
        mov r9, qword [rsp+28h]        ; 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten (8B)
        mov qword [rsp+20h], 0         ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped (8B)
        call WriteFile

        ; 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, 38h                   ; uvolnenie rezervovaneho miesta

Rozdiel v porovnaní s prvou verziou spočíva v rezervovaní miesta v zásobníku tak, aby sa sem vošla lokálna premenná lpNumberOfBytesWritten (8 bajtov), argumenty (8 bajtov pre piaty argument funkcie WriteFile), shadow space (32 bajtov) a zarovnanie (8B). 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:

        sub rsp, 38h                  ; rezervovanie miesta v zasobniku pre shadow space (32B), 5-ty argument funkcie WriteFile (8B), lokalnu premennu lpNumberOfBytesWritten (8B), zarovnanie (8B) ((instrukcia CALL vlozi do zasobnika este navratovu adresu, cize dalsich 8B))

Piaty argument potom samozrejme nie je možné vložiť na požadovanú pozíciu inštrukciou PUSH, ale:

        mov qword [rsp+20h], 0         ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped (8B)

Zásobník je pripravený aj pre funkciu ExitProcess, stačí ju zavolať a až potom uvoľniť rezervované miesto:

        add rsp, 38h                  ; uvolnenie rezervovaneho miesta

Poslednou zmenou je nahradenie inštrukcie

        mov ecx, 0                     ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi

inštrukciou

        xor ecx, ecx                   ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi

Je to pokus o optimalizáciu kódu, kedže inštrukcia xor ecx,ecx tiež vynuluje obsah registra ECX, ale po preložení zaberá menej bajtov. Optimalizácia kódu je však dnes kvôli prúdovému spracovaniu inštrukcií (pipelining), hyper-threading, cache pamäti, atď mimoriadne zložitá a vyžaduje hlboké znalosti. Spravidla dobrý kompilátor/optimalizátor jazyka C dokáže vytvoriť rýchlejší kód než hoci aj kratší ale neoptimalizovaný kód v jazyku symbolických adries.[4][5][6]

Výpis 2b HelloWorld.s (Verzia pre GAS):

# HelloWorld.s

# kompilacia:
#   as -o HelloWorld.o HelloWorld.s
# linkovanie:
#   ld -e main -s -o HelloWorld.exe HelloWorld.o 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 .text
message:                  .ascii  "Hello, World!\15\12"
MESSAGE_LEN = . - message

main:
        sub $0x38, %rsp                  # rezervovanie miesta v zasobniku pre shadow space (32B), 5-ty argument funkcie WriteFile (8B), lokalnu premennu lpNumberOfBytesWritten (8B), zarovnanie (8B) ((instrukcia CALL vlozi do zasobnika este navratovu adresu, cize dalsich 8B))

        /* 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 (8B)
        mov $message, %rdx               # 2. param _In_ LPCVOID lpBuffer (8B)
        mov $MESSAGE_LEN, %r8d           # 3. param _In_ DWORD nNumberOfBytesToWrite (4B)
        mov 0x28(%rsp), %r9              # 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten (8B)
        movq $0, 0x20(%rsp)              # 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped (8B)
        call WriteFile

        /* 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 (4B)
        call ExitProcess

        add $0x38, %rsp                  # uvolnenie rezervovaneho miesta

Za povšimnutie stojí zápis nepriamej adresácie (používanej napríklad pri indexovaní poľa). Kým v NASM to bolo: mov qword [rsp + 20h], 0, v GAS je "index" uvedený pred zátvorkou: movq $0, 0x20(%rsp)

V prípade adresy nejakej premennej je situácia identická:

NASM: mov [premenna + rdi * 4], eax

GAS: mov %eax, premenna( , %rdi, 4)

V prípade registrov:

NASM: mov eax, [rbp + rsi]

GAS: mov (%rbp, %rsi, 1), %eax

Hello, World! v.3

[upraviť]

Posledná verzia programu Hello, World vypíše text pomocou funkcie štandardnej knižnce jazyka C printf.

Výpis 3a HelloWorld.asm:

; HelloWorld.asm

; kompilacia:
;   nasm -f win64 HelloWorld.asm
; linkovanie:
;   ld -e main -s -o HelloWorld.exe HelloWorld.obj C:\opt\mingw64\x86_64-w64-mingw32\lib\libmsvcrt.a
; alternativne linkovanie:
;   gcc -nostartfiles  -Wl,-emain,-s -o HelloWorld.exe HelloWorld.obj C:\opt\mingw64\x86_64-w64-mingw32\lib\libmsvcrt.a


global main

extern  printf


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

main:
        sub rsp, 28h                   ; rezervovanie miesta v zasobniku pre shadow space a zarovnanie

        ; int printf(const char *format, ...)
        ; rax = printf(message)
        mov rcx, qword message
        call printf

        add rsp, 28h                   ; uvolnenie rezervovaneho miesta

        ret

Ako už bolo spomenuté vyššie, volanie funkcie printf z libmsvcrt.a v 64-bitovom windows tiež používa volaciu konvenciu Microsoft x64[3].

Rozširujeme

[upraviť]

IntToStr

[upraviť]

V programe HelloWorld sme do konzolového okna vypísali jednoduchý text. V nasledujúcom programe IntToStr sa pokúsime vypísať kladné celé číslo.

Celé číslo je uložené v pamäti v dvojkovej sústave. My ho samozrejme chceme vypísať v desiatkovej sústave. K tomu potrebujeme zistiť jeho jednotlivé cifry a nájsť ich zodpovedajúci ascii znak. Algoritmus je jednoduchý: Číslo vydelíme (celočíselne) desiatimi. S neúplným podielom budeme opakovať delenie až kým nebude rovný nule, zvyšok po delení bude postupne obsahovať jednotlivé cifry - najskôr jednotky, potom desiatky, stovky, atď.

Napríklad vydelením čísla 321 : 10 dostaneme neúplný podiel 32, zvyšok je 1. Pokračujeme v delení s neúplným podielom 32 : 10 = 3, zvyšok 2, a nakoniec 3 : 10 = 0, zvyšok 3. Zvyšok postupne obsahoval jednotlivé cifry 1, 2, 3, ktoré musíme previesť na zodpovedajúci ASCII znak.

Výpis 4a IntToStr.asm:

; IntToStr.asm

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


global main

extern GetStdHandle
extern WriteFile
extern ExitProcess


        section .data use64           ; Program code
buffer:         times 20 db      " "  ; Najvacsie 64-bitove cislo bez znamienka ma 20 cifier (2**64 - 1 = 18446744073709551615)
enter:                   db      0xd,0xa
lpNumberOfBytesWritten:  dd      0
BUFFER_LEN:              equ     enter-buffer
NEWLINE_LEN:             equ     lpNumberOfBytesWritten-enter


        section .text use64           ; Program code
main:

IntToStr:
        mov rax, 1234567890           ; cislo, ktore potrebujeme vypisat (delenec)
        mov rbx, 10                   ; zaklad ciselnej sustavy (delitel)

        lea rdi, [buffer+BUFFER_LEN-1] ; nastavi register rdi na koniec buffera (cifry budeme ziskavat smerom od najnizsieho radu k najvyssiemu)

vydel:
        xor rdx, rdx                  ; pred delenim je nutne rdx vynulovat, inak delenie skonci chybou
        div rbx                       ; vydeli rax / rbx, podiel vlozi do rax, zvysok do rdx
        add dl, '0'                   ; pripocitanim 30h prevedie cislo 0-9 na znak '0'-'9'
        mov byte [rdi], dl            ; ulozi ziskanu cifru do buffera
        sub rdi, 1                    ; posunie ukazovatel na dalsi rad
        or rax, rax
        jnz vydel                     ; opakuje, kym neziska vsetky cifry

vypis:
        sub rsp, 28h                  ; rezervovanie miesta v zasobniku pre 5-ty argument, shadow space a zarovnanie

        ; 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 buffer          ; 2. param _In_ LPCVOID lpBuffer
        mov r8d, dword BUFFER_LEN+NEWLINE_LEN  ; 3. param _In_ DWORD nNumberOfBytesToWrite
        mov r9, lpNumberOfBytesWritten ; 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten
        mov qword [rsp+20h], 0         ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped
        call WriteFile

        ; 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

IntToHex

[upraviť]

Výpis 5b IntToHex.s:

# IntToHex.s

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

        .global main

        .section .data
buffer:                   .ascii  "                "  /* Najvacsie 64-bitove cislo bez znamienka ma v setnastkovej sustave 16 cifier (2**64 - 1 = ffffffffffffffff) */
enter:                    .ascii  "h\r\n"
lpNumberOfBytesWritten:   .long   0
BUFFER_LEN =              enter - buffer
NEWLINE_LEN =             lpNumberOfBytesWritten - enter


        .section .text

main:

IntToHex:
        mov $1234567890, %rax            /* cislo, ktore potrebujeme vypisat (delenec) */
        mov $16, %rbx                    /* zaklad ciselnej sustavy (delitel) */

        lea (buffer+BUFFER_LEN-1), %rdi  /* nastavi register rdi na koniec buffera (cifry budeme ziskavat smerom od najnizsieho radu k najvyssiemu) */

vydel:
        xor %rdx, %rdx                   /* pred delenim je nutne rdx vynulovat, inak delenie skonci chybou */
        div %rbx                         /* vydeli rax / rbx, podiel vlozi do rax, zvysok do rdx */

        cmp $10, %dl                     /* zistime, ci zvysok je mensi nez 10 */
        jl  doDesat
        add $7, %dl                      /* medzi znakom '9' a 'A' lezi v ASCII sedem znakov, ktore potrebujeme pri prevode na znak 'A'-'F' preskocit */

doDesat:
        add $'0', %dl                    /* pripocitanim 30h prevedie cislo 0-9 na znak '0'-'9' */
        mov %dl, (%rdi)                  /* ulozi ziskanu cifru do buffera */
        sub $1, %rdi                     /* posunie ukazovatel na dalsi rad */
        or %rax, %rax
        jnz vydel                        /* opakuje, kym neziska vsetky cifry */

vypis:
        sub $0x28, %rsp                  /* rezervovanie miesta v zasobniku pre 5-ty argument, shadow space a zarovnanie */

        # 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, $buffer, $BUFFER_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 $buffer, %rdx                /* 2. param _In_ LPCVOID lpBuffer */
        mov $BUFFER_LEN+NEWLINE_LEN, %r8d /* 3. param _In_ DWORD nNumberOfBytesToWrite */
        mov $lpNumberOfBytesWritten, %r9 /* 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten */
        movq $0, 0x20(%rsp)              /* 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped */
        call WriteFile

        # 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 $0x28, %rsp                  /* uvolnenie rezervovaneho miesta */

Ak chceme vypísať číslo v inej, napríklad šestnástkovej sústave, stačí deliť príslušným základom číselnej sústavy:

        mov $16, %rbx                    /* zaklad ciselnej sustavy (delitel) */

Tiež je potrebné vysporiadať sa so znakmi ':', ';', '<', '=', '>', '?' a '@', nachádzajúcimi sa v ASCII medzi znakmi '9' a 'A':

        cmp $10, %dl                     /* zistime, ci zvysok je mensi nez 10 */
        jl  doDesat
        add $7, %dl                      /* medzi znakom '9' a 'A' lezi v ASCII sedem znakov, ktore potrebujeme pri prevode na znak 'A'-'F' preskocit */

doDesat:
        add $'0', %dl                    /* pripocitanim 30h prevedie cislo 0-9 na znak '0'-'9' */

Referencie

[upraviť]
  1. Ram Narayan (2007-10-17). "Linux assemblers: A comparison of GAS and NASM"
  2. Ralf Brown's Interrupt List
  3. 3,0 3,1 https://en.wikipedia.org/wiki/X86_calling_conventions#List_of_x86_calling_conventions
  4. https://www.zive.cz/clanky/vyznejte-se-v-procesoru--velky-prehled-technologii/historie-procesoru-cache-a-skalarni-procesory/sc-3-a-147124-ch-66129/default.aspx#articleStart
  5. http://frdsa.fri.uniza.sk/~janosik/Kniha/Prudove_sprac.html
  6. https://forum.root.cz/index.php?topic=2388.0

Ďalšie zdroje

[upraviť]

Pozri aj

[upraviť]