Otvoriť hlavné menu

Programovanie v assembleri vo Windows x64 (x86-64)

Stručný prehľadUpraviť

Syntax a kompiláciaUpraviť

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 WinAPIUpraviť

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

 1 ; HelloWorld.asm
 2 
 3 ; kompilacia:
 4 ;   nasm -f win64 HelloWorld.asm
 5 ; linkovanie:
 6 ;   golink /console /ni /entry main HelloWorld.obj kernel32.dll
 7 ; alternativne linkovanie:
 8 ;   ld -e main -s -o HelloWorld.exe HelloWorld.obj c:\windows\system32\kernel32.dll
 9 
10 
11 global main
12 
13 extern GetStdHandle
14 extern WriteFile
15 extern ExitProcess
16 
17 
18         section .bss use64             ; neinicializovana datova oblast
19 lpNumberOfBytesWritten:  resd      1
20 
21 
22         section .text use64            ; Program code
23 message:                 db      "Hello, World!",0xd,0xa
24 MESSAGE_LEN:             equ     $-message
25 
26 main:
27         ; rax = GetStdHandle(-11)
28         ; HANDLE hStdHandle = WINAPI GetStdHandle (_In_ DWORD nStdHandle)
29         ; nStdHandle: STD_INPUT_HANDLE=-10 , STD_OUTPUT_HANDLE=-11 , STD_ERROR_HANDLE=-12
30         mov ecx, -11                   ; 1. param _In_ DWORD nStdHandle
31         call GetStdHandle
32 
33         ; rax = WriteFile(%rax, $message, $MESSAGE_LEN, %rsp-4, 0)
34         ; BOOL bErrorFlag = WINAPI WriteFile (_In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped)
35         ; WriteConsole(handle, &msg[0], 13, &written, 0)
36         mov rcx, rax                   ; 1. param _In_ HANDLE hFile
37         mov rdx, qword message         ; 2. param _In_ LPCVOID lpBuffer
38         mov r8d, dword MESSAGE_LEN     ; 3. param _In_ DWORD nNumberOfBytesToWrite
39         mov r9, lpNumberOfBytesWritten ; 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten
40         push qword 0                   ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped
41         call WriteFile
42         add rsp, 8                     ; uvolnenie miesta v zasobniku
43 
44         ; ExitProcess(0)
45         ; VOID WINAPI ExitProcess( _In_ UINT uExitCode)
46         mov ecx, 0                     ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
47         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

30         mov ecx, -11
31         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

36         mov rcx, rax                   ; 1. param _In_ HANDLE hFile
37         mov rdx, qword message         ; 2. param _In_ LPCVOID lpBuffer
38         mov r8d, dword MESSAGE_LEN     ; 3. param _In_ DWORD nNumberOfBytesToWrite
39         mov r9, lpNumberOfBytesWritten ; 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten
40         push qword 0                   ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped
41         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

46         mov ecx, 0                     ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
47         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):

 1 # HelloWorld.s
 2 
 3 # kompilacia:
 4 #   as -o HelloWorld.o HelloWorld.s
 5 # linkovanie:
 6 #   ld -e main -s -o HelloWorld.exe HelloWorld.o c:\windows\system32\kernel32.dll
 7 # alternativna kompilacia+linkovanie:
 8 #   gcc -m64 -nostartfiles -Wl,-emain -o HelloWorld.exe HelloWorld.s c:\windows\system32\kernel32.dll
 9 
10         .global main
11 
12         .section .bss
13 lpNumberOfBytesWritten:   .space      4
14 
15         .section .text
16 message:                  .ascii  "Hello, World!\r\n"
17 MESSAGE_LEN = . - message
18 
19 main:
20         # rax = GetStdHandle(-11)
21         # HANDLE hStdHandle = WINAPI GetStdHandle (_In_ DWORD nStdHandle)
22         # nStdHandle: STD_INPUT_HANDLE=-10 , STD_OUTPUT_HANDLE=-11 , STD_ERROR_HANDLE=-12
23         mov $-11, %ecx                   /* 1. param _In_ DWORD nStdHandle */
24         call GetStdHandle
25 
26         # rax = WriteFile(%rax, $message, $MESSAGE_LEN, %rsp-4, 0)
27         # BOOL bErrorFlag = WINAPI WriteFile (_In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped)
28         # WriteConsole(handle, &msg[0], 13, &written, 0)
29         mov %rax, %rcx                   /* 1. param _In_ HANDLE hFile */
30         mov $message, %rdx               /* 2. param _In_ LPCVOID lpBuffer */
31         mov $MESSAGE_LEN, %r8d           /* 3. param _In_ DWORD nNumberOfBytesToWrite */
32         mov $lpNumberOfBytesWritten, %r9 /* 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten */
33         pushq $0                         /* 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped */
34         call WriteFile
35         add $8, %rsp                     /* uvolnenie miesta v zasobniku */
36 
37         # ExitProcess(0)
38         # VOID WINAPI ExitProcess( _In_ UINT uExitCode)
39         mov $0, %ecx                     /* 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi */
40         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íkUpraviť

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.

PodprogramUpraviť

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 x64Upraviť

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

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

Výpis 2a HelloWorld.asm (Verzia pre NASM):

 1 ; HelloWorld.asm
 2 
 3 ; kompilacia:
 4 ;   nasm -f win64 HelloWorld.asm
 5 ; linkovanie:
 6 ;   golink /console /ni /entry main HelloWorld.obj kernel32.dll
 7 ; alternativne linkovanie:
 8 ;   ld -e main -s -o HelloWorld.exe HelloWorld.obj c:\windows\system32\kernel32.dll
 9 
10 
11 global main
12 
13 extern GetStdHandle
14 extern WriteFile
15 extern ExitProcess
16 
17 
18         section .text use64            ; Program code
19 message:                 db      "Hello, World!",0xd,0xa
20 MESSAGE_LEN:             equ     $-message
21 
22 main:
23         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))
24 
25         ; rax = GetStdHandle(-11)
26         ; HANDLE hStdHandle = WINAPI GetStdHandle (_In_ DWORD nStdHandle)
27         ; nStdHandle: STD_INPUT_HANDLE=-10 , STD_OUTPUT_HANDLE=-11 , STD_ERROR_HANDLE=-12
28         mov ecx, -11                  ; 1. param _In_ DWORD nStdHandle
29         call GetStdHandle
30 
31         ; rax = WriteFile(%rax, $message, $MESSAGE_LEN, %rsp-4, 0)
32         ; BOOL bErrorFlag = WINAPI WriteFile (_In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped)
33         ; WriteConsole(handle, &msg[0], 13, &written, 0)
34         mov rcx, rax                   ; 1. param _In_ HANDLE hFile (8B)
35         mov rdx, qword message         ; 2. param _In_ LPCVOID lpBuffer (8B)
36         mov r8d, dword MESSAGE_LEN     ; 3. param _In_ DWORD nNumberOfBytesToWrite (4B)
37         mov r9, qword [rsp+28h]        ; 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten (8B)
38         mov qword [rsp+20h], 0         ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped (8B)
39         call WriteFile
40 
41         ; ExitProcess(0)
42         ; VOID WINAPI ExitProcess( _In_ UINT uExitCode)
43         xor ecx, ecx                   ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
44         call ExitProcess
45 
46         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:

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

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

46         add rsp, 38h                  ; uvolnenie rezervovaneho miesta

Poslednou zmenou je nahradenie inštrukcie

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

inštrukciou

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

 1 # HelloWorld.s
 2 
 3 # kompilacia:
 4 #   as -o HelloWorld.o HelloWorld.s
 5 # linkovanie:
 6 #   ld -e main -s -o HelloWorld.exe HelloWorld.o c:\windows\system32\kernel32.dll
 7 # alternativna kompilacia+linkovanie:
 8 #   gcc -m64 -nostartfiles -Wl,-emain -o HelloWorld.exe HelloWorld.s c:\windows\system32\kernel32.dll
 9 
10         .global main
11 
12         .section .text
13 message:                  .ascii  "Hello, World!\15\12"
14 MESSAGE_LEN = . - message
15 
16 main:
17         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))
18 
19         /* rax = GetStdHandle(-11) */
20         /* HANDLE hStdHandle = WINAPI GetStdHandle (_In_ DWORD nStdHandle) */
21         /* nStdHandle: STD_INPUT_HANDLE=-10 , STD_OUTPUT_HANDLE=-11 , STD_ERROR_HANDLE=-12 */
22         mov $-11, %ecx                   # 1. param _In_ DWORD nStdHandle
23         call GetStdHandle
24 
25         /* rax = WriteFile(%rax, $message, $MESSAGE_LEN, %rsp-4, 0) */
26         /* BOOL bErrorFlag = WINAPI WriteFile (_In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped) */
27         /* WriteConsole(handle, &msg[0], 13, &written, 0)*/
28         mov %rax, %rcx                   # 1. param _In_ HANDLE hFile (8B)
29         mov $message, %rdx               # 2. param _In_ LPCVOID lpBuffer (8B)
30         mov $MESSAGE_LEN, %r8d           # 3. param _In_ DWORD nNumberOfBytesToWrite (4B)
31         mov 0x28(%rsp), %r9              # 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten (8B)
32         movq $0, 0x20(%rsp)              # 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped (8B)
33         call WriteFile
34 
35         /* ExitProcess(0) */
36         /* VOID WINAPI ExitProcess( _In_ UINT uExitCode) */
37         xor %ecx, %ecx                   # 1. param _In_ UINT uExitCode, UINT je 32 bit aj v 64 bitovom prostredi (4B)
38         call ExitProcess
39 
40         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.3Upraviť

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

Výpis 3a HelloWorld.asm:

 1 ; HelloWorld.asm
 2 
 3 ; kompilacia:
 4 ;   nasm -f win64 HelloWorld.asm
 5 ; linkovanie:
 6 ;   ld -e main -s -o HelloWorld.exe HelloWorld.obj C:\opt\mingw64\x86_64-w64-mingw32\lib\libmsvcrt.a
 7 ; alternativne linkovanie:
 8 ;   gcc -nostartfiles  -Wl,-emain,-s -o HelloWorld.exe HelloWorld.obj C:\opt\mingw64\x86_64-w64-mingw32\lib\libmsvcrt.a
 9 
10 
11 global main
12 
13 extern  printf
14 
15 
16         section .text use64            ; Program code
17 message:                 db      "Hello, World!",0xd,0xa,0
18 
19 main:
20         sub rsp, 28h                   ; rezervovanie miesta v zasobniku pre shadow space a zarovnanie
21 
22         ; int printf(const char *format, ...)
23         ; rax = printf(message)
24         mov rcx, qword message
25         call printf
26 
27         add rsp, 28h                   ; uvolnenie rezervovaneho miesta
28 
29         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širujemeUpraviť

IntToStrUpraviť

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:

 1 ; IntToStr.asm
 2 
 3 ; kompilacia:
 4 ;   nasm -f win64 IntToStr.asm
 5 ; linkovanie:
 6 ;   golink /console /ni /entry main IntToStr.obj kernel32.dll
 7 ; alternativne linkovanie:
 8 ;   ld -e main -s -o IntToStr.exe IntToStr.obj c:\windows\system32\kernel32.dll
 9 
10 
11 global main
12 
13 extern GetStdHandle
14 extern WriteFile
15 extern ExitProcess
16 
17 
18         section .data use64           ; Program code
19 buffer:         times 20 db      " "  ; Najvacsie 64-bitove cislo bez znamienka ma 20 cifier (2**64 - 1 = 18446744073709551615)
20 enter:                   db      0xd,0xa
21 lpNumberOfBytesWritten:  dd      0
22 BUFFER_LEN:              equ     enter-buffer
23 NEWLINE_LEN:             equ     lpNumberOfBytesWritten-enter
24 
25 
26         section .text use64           ; Program code
27 main:
28 
29 IntToStr:
30         mov rax, 1234567890           ; cislo, ktore potrebujeme vypisat (delenec)
31         mov rbx, 10                   ; zaklad ciselnej sustavy (delitel)
32 
33         lea rdi, [buffer+BUFFER_LEN-1] ; nastavi register rdi na koniec buffera (cifry budeme ziskavat smerom od najnizsieho radu k najvyssiemu)
34 
35 vydel:
36         xor rdx, rdx                  ; pred delenim je nutne rdx vynulovat, inak delenie skonci chybou
37         div rbx                       ; vydeli rax / rbx, podiel vlozi do rax, zvysok do rdx
38         add dl, '0'                   ; pripocitanim 30h prevedie cislo 0-9 na znak '0'-'9'
39         mov byte [rdi], dl            ; ulozi ziskanu cifru do buffera
40         sub rdi, 1                    ; posunie ukazovatel na dalsi rad
41         or rax, rax
42         jnz vydel                     ; opakuje, kym neziska vsetky cifry
43 
44 vypis:
45         sub rsp, 28h                  ; rezervovanie miesta v zasobniku pre 5-ty argument, shadow space a zarovnanie
46 
47         ; rax = GetStdHandle(-11)
48         ; HANDLE hStdHandle = WINAPI GetStdHandle (_In_ DWORD nStdHandle)
49         ; nStdHandle: STD_INPUT_HANDLE=-10 , STD_OUTPUT_HANDLE=-11 , STD_ERROR_HANDLE=-12
50         mov ecx, -11                  ; 1. param _In_ DWORD nStdHandle
51         call GetStdHandle
52 
53         ; rax = WriteFile(%rax, $message, $MESSAGE_LEN, %rsp-4, 0)
54         ; BOOL bErrorFlag = WINAPI WriteFile (_In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped)
55         ; WriteConsole(handle, &msg[0], 13, &written, 0)
56         mov rcx, rax                   ; 1. param _In_ HANDLE hFile
57         mov rdx, qword buffer          ; 2. param _In_ LPCVOID lpBuffer
58         mov r8d, dword BUFFER_LEN+NEWLINE_LEN  ; 3. param _In_ DWORD nNumberOfBytesToWrite
59         mov r9, lpNumberOfBytesWritten ; 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten
60         mov qword [rsp+20h], 0         ; 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped
61         call WriteFile
62 
63         ; ExitProcess(0)
64         ; VOID WINAPI ExitProcess( _In_ UINT uExitCode)
65         xor ecx, ecx                   ; 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi
66         call ExitProcess
67 
68         add rsp, 28h                   ; uvolnenie rezervovaneho miesta

IntToHexUpraviť

Výpis 5b IntToHex.s:

 1 # IntToHex.s
 2 
 3 # kompilacia:
 4 #   as -o IntToHex.o IntToHex.s
 5 # linkovanie:
 6 #   ld -e main -s -o IntToHex.exe IntToHex.o c:\windows\system32\kernel32.dll
 7 # alternativna kompilacia+linkovanie:
 8 #   gcc -m64 -nostartfiles -Wl,-emain -o IntToHex.exe IntToHex.s c:\windows\system32\kernel32.dll
 9 
10         .global main
11 
12         .section .data
13 buffer:                   .ascii  "                "  /* Najvacsie 64-bitove cislo bez znamienka ma v setnastkovej sustave 16 cifier (2**64 - 1 = ffffffffffffffff) */
14 enter:                    .ascii  "h\r\n"
15 lpNumberOfBytesWritten:   .long   0
16 BUFFER_LEN =              enter - buffer
17 NEWLINE_LEN =             lpNumberOfBytesWritten - enter
18 
19 
20         .section .text
21 
22 main:
23 
24 IntToHex:
25         mov $1234567890, %rax            /* cislo, ktore potrebujeme vypisat (delenec) */
26         mov $16, %rbx                    /* zaklad ciselnej sustavy (delitel) */
27 
28         lea (buffer+BUFFER_LEN-1), %rdi  /* nastavi register rdi na koniec buffera (cifry budeme ziskavat smerom od najnizsieho radu k najvyssiemu) */
29 
30 vydel:
31         xor %rdx, %rdx                   /* pred delenim je nutne rdx vynulovat, inak delenie skonci chybou */
32         div %rbx                         /* vydeli rax / rbx, podiel vlozi do rax, zvysok do rdx */
33 
34         cmp $10, %dl                     /* zistime, ci zvysok je mensi nez 10 */
35         jl  doDesat
36         add $7, %dl                      /* medzi znakom '9' a 'A' lezi v ASCII sedem znakov, ktore potrebujeme pri prevode na znak 'A'-'F' preskocit */
37 
38 doDesat:
39         add $'0', %dl                    /* pripocitanim 30h prevedie cislo 0-9 na znak '0'-'9' */
40         mov %dl, (%rdi)                  /* ulozi ziskanu cifru do buffera */
41         sub $1, %rdi                     /* posunie ukazovatel na dalsi rad */
42         or %rax, %rax
43         jnz vydel                        /* opakuje, kym neziska vsetky cifry */
44 
45 vypis:
46         sub $0x28, %rsp                  /* rezervovanie miesta v zasobniku pre 5-ty argument, shadow space a zarovnanie */
47 
48         # rax = GetStdHandle(-11)
49         # HANDLE hStdHandle = WINAPI GetStdHandle (_In_ DWORD nStdHandle)
50         # nStdHandle: STD_INPUT_HANDLE=-10 , STD_OUTPUT_HANDLE=-11 , STD_ERROR_HANDLE=-12
51         mov $-11, %ecx                   /* 1. param _In_ DWORD nStdHandle */
52         call GetStdHandle
53 
54         # rax = WriteFile(%rax, $buffer, $BUFFER_LEN, %rsp-4, 0)
55         # BOOL bErrorFlag = WINAPI WriteFile (_In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped)
56         # WriteConsole(handle, &msg[0], 13, &written, 0)
57         mov %rax, %rcx                   /* 1. param _In_ HANDLE hFile */
58         mov $buffer, %rdx                /* 2. param _In_ LPCVOID lpBuffer */
59         mov $BUFFER_LEN+NEWLINE_LEN, %r8d /* 3. param _In_ DWORD nNumberOfBytesToWrite */
60         mov $lpNumberOfBytesWritten, %r9 /* 4. param _Out_opt_ LPDWORD lpNumberOfBytesWritten */
61         movq $0, 0x20(%rsp)              /* 5. param _Inout_opt_ LPOVERLAPPED lpOverlapped */
62         call WriteFile
63 
64         # ExitProcess(0)
65         # VOID WINAPI ExitProcess( _In_ UINT uExitCode)
66         xor %ecx, %ecx                   /* 1. param _In_ UINT uExitCode UINT je 32 bit aj v 64 bitovom prostredi */
67         call ExitProcess
68 
69         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:

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

34         cmp $10, %dl                     /* zistime, ci zvysok je mensi nez 10 */
35         jl  doDesat
36         add $7, %dl                      /* medzi znakom '9' a 'A' lezi v ASCII sedem znakov, ktore potrebujeme pri prevode na znak 'A'-'F' preskocit */
37 
38 doDesat:
39         add $'0', %dl                    /* pripocitanim 30h prevedie cislo 0-9 na znak '0'-'9' */

ReferencieUpraviť

Ďalšie zdrojeUpraviť

Pozri ajUpraviť