Assembler 64-bit

Regulamin działu
Kolorowanie składni :
[c][/c], [vb][/vb], [asm][/asm], [delphi][/delphi], [pascal[/pascal], [python][/python], [perl][/perl], [ruby][/ruby], [bash][/bash]

Assembler 64-bit

Postprzez D.F. 31 gru 2009, o 22:30

Witam,
Jakiś czas temu stałem się posiadaczem systemu Windows 7 (64bit) i zapewne też tutaj są użytkownicy systemów 64-bitowych. Od razu zaciekawiła mnie rzecz, jak wygląda programowanie w Assemblerze pod tym systemem, zatem ruszyłem z pomocą do Google. W tym artykule chciałbym zebrać informacje, które mam nadzieję będą pomocne w rozpoczęciu programowania w systemach 64-bitowych. Materiałów w języku polskim nie znalazłem, to co wiem przeczytałem z anglojęzycznych for i dokumentacji.


1. Wstęp

W systemach 64-bitowych nie jest możliwe już uruchamianie aplikacji dla systemu DOS. Można to zrobić jednak poprzez skorzystanie z emulatora DosBox, jednak jest to spore ograniczenie. Natomiast jeżeli chodzi o aplikacje dla systemu Win32 w większości przypadków powinieneś uruchomić je bezproblemowo z niewielką stratą na wydajności. Umożliwia to nakładka WOW64 (Windows on Windows 64). Loader uruchamiający pliki wykonywalne rozpoznaje czy jest on 32-bitowy. Jeżeli tak, uruchamiany jest on w ten sposób, że wywołania funkcji itp. są "tłumaczone" przez tą nakładkę i przekierowywane do bibliotek systemowych (64 bitowych).
Ilustracja:
Obrazek

Wraz z nową architekturą zwiększyła się przestrzeń adresowa, zmienił się format plików (nazywany PE32+), mamy nowe rejestry procesora, niektóre instrukcje nie są dostępne, ale doszły także nowe.


2. Zmiany w formacie plików wykonywalnych.

Pięć pól struktury pliku wykonywalnego zostało rozszerzonych, jedno zostało usunięte.
Przedstawia to poniższa tabela:
Obrazek


3. Rejestry procesora w architekturze x64

Dotychczas znane rejestry ogólnego przeznaczenia zostały rozszerzone do rozmiaru 64-bity i nazwane z prefixem REX, czyli EAX->RAX, EBX->RBX, ECX->RCX itd. Doszło także osiem nowych rejestrów ogólnego przeznaczenia nazwanych: R8, R9, R10, R11, R12, R13, R14, R15.
Dodane zostały też nowe rejestry 128-bitowe XMM (XMM8-XMM15).

Ilustracja uwzględniająca zmiany w rejestrach procesora:
Obrazek


4. Zmiany w instrukcjach

Instrukcje, które nie są dostępne (wystąpi wyjątek invalid-opcode):
AAA - ASCII Adjust After Addition
AAD - ASCII Adjust AX Before Division
AAM - ASCII Adjust AX After Multiply
AAS - ASCII Adjust AL After Substraction
BOUND - Check Array Bound
DAA - Decimal Adjust After Addition
DAS - Decimal Adjust After Substraction
INTO - Interrupt to Overflow Vector
LDS, LES - Load Far Pointer
POPA, POPAD - Pop All GPRs
POP DS/ES/SS - Pop instruction using DS, ES or SS register.
PUSH CS/DS/ES/SS - Push Onto Stack instruction using CS, DS, ES or SS register
PUSHA, PUSHAD - Push All GPRs onto Stack

Dodatkowo instrukcja SYSENTER/SYS.EXIT (w tej instrukcji nie ma być kropki w środku, ale musiałem tak napisać z powodu, że forum cenzuruje ten wyraz) jest dotępna tylko w tzw. Legacy Mode (inaczej wystąpi invalid-opcode exception).
W trybie Long Mode należy użyć instrukcji SYSCALL/SYSRET.

Instrukcje LODSB, LODSW, LODSD, CMPSB, CMPSQ, MOVSW, MOVSQ, SCASD, SCASQ, STOSQ zostały rozszerzone do obsługi adresów 64-bitowych oraz dodane zostały nowe instrukcje LODSQ, CMPSQ, MOVSQ, SCASQ, STOSQ.

Również instrukcje REP, REPZ, REPNZ, LOOP, LOOPZ, LOOPNZ korzystają teraz z rejestrów 64-bitowych.

Kompletna lista instrukcji:
General-Purpose and System Instructions


5. Konwencja wywołania

Zmieniła się również konwencja wywołania (calling convention). W systemie Win32 była konwencja STDCALL. Tutaj konwencja wywołania nazywa się FASTCALL i zamiast przekazywać parametry przez stos, pierwsze cztery przekazujemy przez specjalne rejestry (RCX, RDX, R8, R9), a resztę przez stos. Dodatkowo musimy zrobić także miejsce na stosie na parametry przekazywane przez rejestry (w praktyce wykonanie [FONT="Courier New"]sub rsp, liczba[/FONT]).
Wywołanie funkcji MessageBox w Win32:
Kod: Zaznacz cały
  1. push 0
  2. push offset szCaption
  3. push offset szText
  4. push 0
  5. call MessageBoxA

W systemie Win64 wywołanie tej samej funkcji wygląda to tak:
Kod: Zaznacz cały
  1. sub rsp, 28h
  2. mov r9d, 0
  3. lea r8, szCaption
  4. lea rdx, szText
  5. mov rcx, 0
  6. call MessageBoxA



6. Adresowanie

W Assemblerze x64 adresy do miejsc (danych i kodu), które znajdują się wewnątrz programu pozostają 32-bitowe, jest tak przez relatywne adresowanie (RIP-relative addressing). My podajemy adres pośredni, z którego adres bezpośredni zostaje utworzony dodając adres pośredni do aktualnego adresu. Jeżeli odwołujemy się do adresów zewnętrznych, np. funkcji z jakiejś biblioteki, adres wtedy jest 64-bitowy.


7. Kompilatory Assemblera x64

- MASM64
Jeżeli chodzi o kompilatory to możemy korzystać z MASM64 (ml64.exe) dołączonego do WINDDK (Windows Driver Development Kit) w katalogu:
\WINDDK\3790.1830\bin\win64\x86\amd64

Polecam jednak ściągnąć paczkę: kompilator wraz z includami i kilkoma przykładami:
MASM64 SDK

- jWasm
Możemy również skorzystać z tego kompilatora. Jest on kompatybilny z MASMem.
Oficjalna strona jWasm

- NASM
Assembler NASM od wersji 2.00 wspiera architekture 86-x64:
Strona assemblera NASM

- FASM
Z tego co możemy przeczytać na stronie, FASM również wspiera architekturę x64.
Strona assemblera FASM

- GoAsm
Strona GoAsm


8. Debuggery aplikacji 64-bitowych

- Debugging Tools for Windows 64-bit Version
- FDBG - AMD64 Assembly Debugger


9. Zakończenie

To już wszystko w tym artykule.
Tekst powstał na bazie mojego wcześniejszego artykułu: Wstęp do Assemblera dla architektury 64-bit

Na koniec kilka linków:
- CodeProject: Moving to Windows Vista x64
- AMD64 Architecture Tech Docs
- Microsoft Macro Assembler Reference - MASM for x64
- GoAsm: Writing 64-bit programs
- jWasm: x64 Hello World

//EDIT: Poprawiłem wartość przy wywołaniu MessageBoxA.
Ostatnio edytowano 9 lut 2010, o 12:05 przez D.F., łącznie edytowano 4 razy

D.F.
 
Posty: 13
Dołączył(a): 1 lis 2009, o 19:42
Lokalizacja: Polska

Re: Assembler 64-bit

Postprzez Gynvael Coldwind 2 sty 2010, o 12:37

Cześć!

Thx za arta ;> Przyznaje że assembler x86-64 cały czas leży na mojej liście TODO nieruszony, a Twój post stał się motywacją, żeby w końcu się za to wziąć.

Po przeczytaniu Twojego posta mam w zasadzie jedną uwagę, dotyczącą paragrafu "5. Konwencja wywołania".

D.F napisał(a):Dodatkowo musimy zrobić także miejsce na stosie na parametry przekazywane przez rejestry (w praktyce wykonanie sub rsp, liczba).

[...]

W systemie Win64 wywołanie tej samej funkcji wygląda to tak:

Kod: Zaznacz cały
  1. sub rsp, 28h
  2. mov r9d, 0
  3. lea r8, szCaption
  4. lea rdx, szText
  5. mov rcx, 0
  6. call MessageBoxA



Szczerze to nie za bardzo widzę potrzebę alokacji 28h na stosie przed wywołaniem funkcji. Po pierwsze, jak napisałeś, parametry są w rejestrach, a nie na stosie. Normalnym jest, że jeżeli funkcja wywoływana chce mieć te parametry na stosie, to sobie sama zaalokuje odpowiednią ilość miejsca gdzie jej wygodnie. Po za tym skąd by się 28 brało ? Parametry są 4, więc powinno być 20 raczej ? Chyba że 8 bajtów miało by iść na adres RET, co raczej sensu nie ma, ponieważ (co można zobaczyć w tomie 2a manuali Intela) w Long Mode call nadal wrzuca RET na stos, więc de facto sam sobie alokuje miejsce na stosie na adres powrotu.

Po przejrzeniu materiałów źródłowych które podałeś, jestem zdania, że zasugerowałeś się kodem z CodeProject:
Kod: Zaznacz cały
  1. .text:0000000000401220 sub_401220 proc near       ; CODE XREF: start+10E p
  2.  
  3. .text:0000000000401220
  4. .text:0000000000401220 arg_0= qword ptr 8
  5. .text:0000000000401220 arg_8= qword ptr 10h
  6. .text:0000000000401220 arg_10= qword ptr 18h
  7. .text:0000000000401220 arg_18= dword ptr 20h
  8. .text:0000000000401220
  9. .text:0000000000401220    mov [rsp+arg_18], r9d
  10. .text:0000000000401225    mov [rsp+arg_10], r8
  11. .text:000000000040122A    mov [rsp+arg_8], rdx
  12. .text:000000000040122F    mov [rsp+arg_0], rcx
  13. .text:0000000000401234    sub rsp, 28h
  14. .text:0000000000401238    xor r9d, r9d            ; uType
  15. .text:000000000040123B    lea r8, Caption         ; "My First x64 Application"
  16. .text:0000000000401242    lea rdx, Text           ; "Hello World!"
  17. .text:0000000000401249    xor ecx, ecx            ; hWnd
  18. .text:000000000040124B    call cs:MessageBoxA
  19. .text:0000000000401251    xor eax, eax
  20. .text:0000000000401253    add rsp, 28h
  21. .text:0000000000401257    retn
  22. .text:0000000000401257 sub_401220 endp


Tutaj jednak to sub rsp, 28h należy do inicjacji ramki stosu (identycznie jak w x86), a nie do wywołania funkcji MessageBoxA.
I szczerze mówiąc, po rzuceniu okiem w kod MessageBoxA, jestem raczej zdania, że kompilator użyty w powyższym przykładzie nie jest przyzwyczajony do fastcall, i pewne alokacje robi zupełnie niepotrzebnie.

Pozdrawiam serdecznie! :)
gynvael.coldwind//vx
Tech Blog: http://gynvael.coldwind.pl
ReverseCraft: http://re.coldwind.pl

Avatar użytkownika
Gynvael Coldwind
 
Posty: 35
Dołączył(a): 15 sie 2009, o 21:42

Re: Assembler 64-bit

Postprzez D.F. 3 sty 2010, o 15:38

Gynvael Coldwind napisał(a):Szczerze to nie za bardzo widzę potrzebę alokacji 28h na stosie przed wywołaniem funkcji. Po pierwsze, jak napisałeś, parametry są w rejestrach, a nie na stosie. Normalnym jest, że jeżeli funkcja wywoływana chce mieć te parametry na stosie, to sobie sama zaalokuje odpowiednią ilość miejsca gdzie jej wygodnie.

Alokacja miejsca na stosie na parametry przesyłane przez rejestry jest właśnie naszym zadaniem.

Gynvael Coldwind napisał(a):Po za tym skąd by się 28 brało ? Parametry są 4, więc powinno być 20 raczej ? Chyba że 8 bajtów miało by iść na adres RET, co raczej sensu nie ma, ponieważ (co można zobaczyć w tomie 2a manuali Intela) w Long Mode call nadal wrzuca RET na stos, więc de facto sam sobie alokuje miejsce na stosie na adres powrotu.

//EDIT
Ostatnio edytowano 3 sty 2010, o 21:33 przez D.F., łącznie edytowano 2 razy

D.F.
 
Posty: 13
Dołączył(a): 1 lis 2009, o 19:42
Lokalizacja: Polska

Re: Assembler 64-bit

Postprzez Gynvael Coldwind 3 sty 2010, o 19:54

To nadal nie ma sensu dla mnie. Po co alokować na stosie miejsce na coś co wysyłasz w rejestrze?
Przecież np. w x86 mamy analogiczną sytuację, tj. zmienne typu float które nie są umieszczane na normalnym stosie, tylko w rejestrach FPU (ST0...ST7). Dla nich nie alokujemy żadnego miejsca na stosie.
To samo się tyczy konwencji fastcall, która przecież też jest obecna na x86, i w której mowy nie ma o alokacji miejsca na stosie.

Podsumowując, po co alokujemy miejsce w x86-64 dla czegoś co nie jest przekazywane stosem ?
gynvael.coldwind//vx
Tech Blog: http://gynvael.coldwind.pl
ReverseCraft: http://re.coldwind.pl

Avatar użytkownika
Gynvael Coldwind
 
Posty: 35
Dołączył(a): 15 sie 2009, o 21:42

Re: Assembler 64-bit

Postprzez D.F. 3 sty 2010, o 21:19

To nie jest ta konwencja co w architekturze 32-bitowej. Jest to inny rodzaj Fastcall. Tutaj musimy zaalokować miejsce na parametry przesyłane przez rejestry i tego jestem pewny.
Spójrz na ilustracje z dokumentacji, przedstawiające jak wygląda stos:
Obrazek

Tylko jeszcze nie daje mi spokoju sprawa tej wartości. Dla czterech parametrów wychodzi wartość 20h, wartość ta jest już wyrównana do 16 bajtów (20h=32d co jest podzielne przez 16), czyli dochodzi jeszcze 8 bajtów na adres powrotny?
Będę musiał to dokładniej zbadać.

D.F.
 
Posty: 13
Dołączył(a): 1 lis 2009, o 19:42
Lokalizacja: Polska

Re: Assembler 64-bit

Postprzez Gynvael Coldwind 3 sty 2010, o 21:39

OK, tak jest w dokumentacji którą cytujesz (której? nie jestem przekonany czy to powinno być interepretowane w tak dosłowny sposób).
Natomiast dlaczego tak jest? Jaki jest sens alokacji miejsca na stosie, które i tak nie jest używane?

EDIT: OK, sam sobie odpowiedziałem post niżej ;>
Ostatnio edytowano 3 sty 2010, o 22:01 przez Gynvael Coldwind, łącznie edytowano 1 raz
gynvael.coldwind//vx
Tech Blog: http://gynvael.coldwind.pl
ReverseCraft: http://re.coldwind.pl

Avatar użytkownika
Gynvael Coldwind
 
Posty: 35
Dołączył(a): 15 sie 2009, o 21:42

Re: Assembler 64-bit

Postprzez Gynvael Coldwind 3 sty 2010, o 22:01

OK, znalazłem na MSDN art Mattha Pietreka:
http://msdn.microsoft.com/en-us/magazine/cc300794.aspx

Drilling into the calling convention a bit, even though an argument can be passed in a register, the compiler still reserves space on the stack for it by decrementing the RSP register. At a minimum, each function must reserve 32 bytes (four 64-bit values) on the stack. This space allows registers passed into the function to be easily copied to a well-known stack location. The callee function isn't required to spill the input register params to the stack, but the stack space reservation ensures that it can if needed.


Niemniej jednak nie do końca to mnie przekonało o słuszności tej decyzji projektowej. Potem znalazłem post Raymonda (polecam jego blog, jest kapitalny):
http://blogs.msdn.com/oldnewthing/archi ... 58579.aspx

The first four parameters to a function are passed in rcx, rdx, r8 and r9. Any further parameters are pushed on the stack. Furthermore, space for the register parameters is reserved on the stack, in case the called function wants to spill them; this is important if the function is variadic.


OK, przyznaje, ma to sens w przypadku gdy nawet funkcje typu variadic (tj ze zmienną liczbą argumentów) chce się wywoływać fastcall a nie cdecl ;>

Przy czym ważne jest, że nawet jeśli funkcja nie ma argumentów, to te 32 bajty i tak muszą być zarezerwowane (huh).

Tak więc zwracam honor i czuje się przekonany ;>
Ostatnio edytowano 3 sty 2010, o 23:54 przez Gynvael Coldwind, łącznie edytowano 1 raz
gynvael.coldwind//vx
Tech Blog: http://gynvael.coldwind.pl
ReverseCraft: http://re.coldwind.pl

Avatar użytkownika
Gynvael Coldwind
 
Posty: 35
Dołączył(a): 15 sie 2009, o 21:42

Re: Assembler 64-bit

Postprzez D.F. 3 sty 2010, o 22:35

To ja jeszcze dopiszę coś o tym wyrównaniu stosu, bo nie było to do końca dla mnie jasne i nawet walnąłem się w artykule na stronie :\, co już na szczęście poprawiłem.

Wywołując funkcję MessageBoxA musimy zaalokować na stosie miejsce na cztery parametry. Czyli wydawałoby się, że wykonać sub rsp, 20h. Jednak z powodu tego, że stos musi być wyrównany do 16 bajtów, musimy zaalokować dodatkowe 8 bajtów, gdyż funkcja odkładając na stos adres powrotny powoduje, ze stos nie jest wtedy wyrównany.

Robiąc sub rsp, 28h po odłożeniu na stos przez funkcję adresu powrotnego, stos jest wyrównany do 16 bajtów:
28h (parametry) + 8h (adres powrotny) = 30h = 48d co jest podzielne przez 16.

D.F.
 
Posty: 13
Dołączył(a): 1 lis 2009, o 19:42
Lokalizacja: Polska

Re: Assembler 64-bit

Postprzez Gynvael Coldwind 3 sty 2010, o 23:35

W ramach pytań typu "dlaczego", to pozostało mi jeszcze jedno - tj. dlaczego wymagany jest align do 16 bajtów? Ale na to już sam sobie odpowiem ;>

Na MSDN znalazłem opis konwencji fastcall:
http://msdn.microsoft.com/en-us/library/ms235286.aspx

Most structures are aligned to their natural alignment. The primary exceptions are the stack pointer and malloc or alloca memory, which are aligned to 16 byte, in order to aid performance. Alignment above 16 bytes must be done manually, but since 16 bytes is a common alignment size for XMM operations, this should suffice for most code.


OK, czyli "aid performance". Druga część cytatu brzmiała dla mnie cokolwiek enigmatycznie, więc szukałem jaki to "performance" jest, i dlaczego.

Strzał 1. Cache line - chybiony
Założyłem że cache line może mieć np 16 bajtów, i że intencją twórców jest trzymanie za równo argumentów jak i adresu RET jak i zmiennych lokalny w oddzielnych cache line'ach (miało by to sens, bo zazwyczaj z argumentów korzysta się na początku funkcji, a potem się jedzie ze zmiennych lokalnych).
Natomiast jak się okazało, sensu to nie ma, ponieważ cache line'y L1 mają 32 (pentium, P6) lub 64 (pentium M/4 i Core/Core 2) bajty pamięci per linia cache (więc align powinien chyba być do 64 bajtów) (Intel Manual 3A, rozdział Memory Cache Control)

Strzał 2. Szybszy dostęp do pamięci, gdy adres jest podzielny przez 16 - chybiony
Nope. Align wystarczy do size operandu, czyli dla 8-bajtowych zmiennych, adres wystarczy że jest podzielny przez 8 (Intel Optimization Reference Manual).

Strzał 3. no idea ;D

Na szczęście znalazłem pod koniec w/w manuala dodatek D (Stack Alignment) w którym jest w zasadzie powtórzone to co wcześniej zignorowałem:
It is important to ensure that the stack frame is aligned to a 16-byte boundary upon function entry to keep local __m128 data, parameters, and XMM register spill locations aligned throughout a function invocation.


Czyli jest to w zasadzie taka zapobiegawcza (preemptive) optymalizacja, co by funkcja nie musiała się bawić w alignment w razie użycia SIMD i XMM etc - w końcu architektura __fastcall win64 to zapewnia ;>
gynvael.coldwind//vx
Tech Blog: http://gynvael.coldwind.pl
ReverseCraft: http://re.coldwind.pl

Avatar użytkownika
Gynvael Coldwind
 
Posty: 35
Dołączył(a): 15 sie 2009, o 21:42


Powrót do Assembler

Kto przegląda forum

Użytkownicy przeglądający ten dział: Brak zalogowanych użytkowników i 0 gości

cron