MazeGen, 2007-04-18 | Revize: 1.0 |
Čas od času se objevují úvahy o portabilním asembleru (což je sám o sobě protimluv). Jako programátorovi v asembleru x86 se tyto podivné úvahy nevyhnuly ani mě. Podobnost mezi x86-32 a x64 se prostě nedá přehlédnout a sama vybízí k hledání nějaké společné syntaxe, zde nazvané Portabilní flat syntaxe x86 (PFS).
Uveďme si stručně hlavní novinky na architektuře x64:
MOV [RIP-2008h], RAX
)r8
až
r15
xmm8
až xmm15
Začněme s tím, že všechny tyto nové vlastnosti zakážeme. Tím bude automaticky možné existující 32bitový kód přenést na x64 a vše bude fungovat :-)
Bohužel, tak jednoduše to nejde. Je třeba připočítat nekompatibilní komplikace:
RIP
), takže všechny ukazatele musí také být
64bitové. I když může být celý proces zaveden pod hranici 4GB, kde
by stačily 32bitové ukazatele, ukazatele vrácené operačním
systémem mohou být nad touto hranicí.PUSH EAX
– lze použít pouze PUSH RAX
nebo
PUSH AX
(stejně to platí i pro 32bitový
paměťový operand).Jak je vidět, při návrhu nové společné syntaxe se nejde vyhnout zavedení určitých základních pravidel.
Jaká pravidla navrhnout?
Při použití registru jako ukazatele je ho třeba vždy
používat podobně jako v 64bitovém kódu. Při kompilaci do 32bitového kódu bude
automaticky převeden na 32 bitů. Pro uložení ukazatele do paměti
je třeba používat abstraktní typ, řeknemě
PVOID
. Budou pro něj platit stejná pravidla, jako
pro ukazatel uložený v registru. Tento typ by byl definován nějak
takto: (pseudokód)
IF compile-for-64-bits PVOID typedef QWORD ELSE PVOID typedef DWORD ENDIF
Příklad použití pointeru v PFS:
port_ptr PVOID ? ... mov [port_ptr], rax ; v 32bitovém kódu se z rax stane eax
Obecně je ošetření portability instrukcí jako
PUSH DWORD PTR [foo]
problém. Vynucovat 64bitovou deklaraci proměnné
foo
, když to není nutné, není vhodné
(např. z hlediska nároků na paměť). Stejně tak zavádět speciální
typ, závislý na tom, zda je operand ukládán na zásobník, není
použitelné řešení. Proto v tuto chvíli předpokládejme, že
konkrétní implementace PFS (nazvěme ji třeba
Portabilní x86 flat framework, PFF) by umožňovala
pohodlné používání lokálních proměnných ve funkcích. Tím se
nezbytnost explicitního používání instrukcí PUSH
a POP
ztrácí a jde je v PFS úplně
zakázat. Výjimkou může zůstat instrukce
PUSH const
, která automaticky neznaménkově
rozšíří konstantní hodnotu na šířku zásobníku (tato vlastnost dělá
instrukci částečně portabilní).
Tato dvě pravidla by měla rešit nekompatibility. Je to něco, čemu bysme mohli říkat Minimální portabilní flat syntaxe (MPFS). Zkusme to ale vylepšit a ještě jednou projděme nové vlastnosti, dostupné v 64bitovém kódu:
Nelze explicitně umožnit, v 32bitovém kódu nic podobného
neexistuje. Existuje trik, kterým lze aktuální EIP
zjistit, pro zjednodušení ho ale nepoužijeme. Mimo to je tato
adresace z větší části otázkou samotného assembleru, ne syntaxe
jako takové.
Tyto by šlo umožnit pouze použitím komplikovaných transformací v 32bitovém kódu. Pro jednoduchost toto opět nelze umožnit (pouze u ukazatelů, viz pravidlo číslo 1).
r8
až
r15
xmm8
až xmm15
Lze povolit za předpokladu, že implementace
PFS bude umožňovat
vytváření vláken vlastními prostředky a tím ošetří problémy
s emulací těchto registrů. Dále musí být alespoň jeden registr
z původních všeobecných registrů vyhrazený kvůli umožnění
transformace instrukcí jako MOV EAX, [R8]
pro
32bitový kód na: (pseudokód)
; r8d_reg je emulovaný registr r8d ; tmp_reg je všeobecný registr, vyhrazený pro PFS mov tmp_reg, [r8d_reg] mov eax, [tmp_reg]
Z toho vyplývá, že pokud budeme chtít povolit
i adresaci typu MOV EAX, [R8+R9*4]
, bude
potřeba další vyhrazený registr:
mov tmp_reg1, [r8d_reg] mov tmp_reg2, [r9d_reg] mov eax, [tmp_reg1+tmp_reg2*4]
Jako první vyhrazený registr mě napadá
rBX
, protože ten je implictně použit pouze v instrukci
XLAT
,
která není moc používaná a jde ji snadno nahradit.
S hledáním
dalšího registru je to horší, protože všechny ostatní mají nějaký
zvláštní význam. V tuto chvíli je potřeba si uvědomit, že některé
instrukce mají v obou módech stejnou syntaxi. Pokud k tomu
navíc přidáme pravidlo o nemožnosti používat 64bitové operandy, tak
vyjde najevo, že např. instrukce REP STOSD
má
stejnou syntaxi i v rámci PFS. Pokud jasně stanovíme,
kdy jsou vyhrazené registry použité (přepsané), můžeme jako další
registr vyhradit rCX
, který se hůř nahrazuje pouze
v případě prefixu z rodiny REP
. V tomto případě lze
v PFS bez problémů použít i tento kód:
xor eax, eax lea rdi, [buffer] mov ecx, [rdx] ; pozor, pro adresaci nepoužij žádný z nových registrů (r8, ...) rep stosd
Tím jsme ale ještě pořád nevyřešili všechny
kombinace operandů. Tou nejkomplikovanější kombinací jsou operandy
jako MOV [R8+R9], R10
. V tomto případě by
bylo ideální použít ještě jeden vyhrazený registr:
mov tmp_reg1, [r8d_reg] mov tmp_reg2, [r9d_reg] mov tmp_reg3, [r10d_reg] mov [tmp_reg1+tmp_reg2], tmp_reg3
Protože bychom už těžko mohli rezervovat další registr pro potřeby PFS, použijeme následující řešení:
mov tmp_reg1, [r8d_reg] mov tmp_reg2, [r9d_reg] lea tmp_reg1, [tmp_reg1+tmp_reg2] ; uvolni tmp_reg2 mov tmp_reg2, [r10d_reg] mov [tmp_reg1], tmp_reg2
Registry XMM v žádné instrukci
implicitní použití nemají, proto lze vyhradit např. xmm7
.
Tím máme všechna pravidla daná. Shrnout a upřesnit by se dala nějak takto:
PFS se podobá syntaxi 64bitového kódu s těmito rozdíly:
PVOID
, který zajistí
portabilitu ukazatele.PUSH
a POP
. Jedinou výjimkou je instrukce
PUSH const
, kde const
je nižší nebo
rovna FFFFFFFFh.eBX
, eCX
a xmm7
(viz
následující pravidlo).ebx
. Pokud je navíc
některý z těchto registrů použit jako index, je přepsán registr
ecx
. Pokud je použit některý z nových
XMM registrů, je přepsán registr
xmm7
. Jinak je zaručeno, že tyto registry nebudou
přepsány.Následuje ukázka většiny z těchto pravidel:
port_base PVOID ? port_index DWORD ? ... mov [port_base], rax mov r8, [port_base] mov r9d, [port_index] ; v 64bitovém módu se chová jako movzx add r10w, [buffer+r8+r9*2]
Tento kód bude transformován do 32bitového kódu takto:
mov [port_base], eax ; mov [port_base], rax mov ebx, [port_base] ; mov r8, [port_base] mov [r8d_reg], ebx mov ebx, [port_index] ; mov r9d, [port_index] mov [r9d_reg], ebx mov ebx, [r8d_reg] ; add r10w, [buffer+r8+r9*2] mov ecx, [r9d_reg] mov bx, [buffer+ebx+ecx*2] add word ptr [r10d_reg], bx
Abychom nezůstávali jenom u teorie, popíšu ještě návrh PFF, vytvořeného pomocí maker MASM.
Protože vytváření vláken a funkcí a jejich volání pomocí portabilních maker je dost komplikovaná záležitost, ukázka obsahuje pouze způsob, jak vytvořit portabilní kód hlavního vlákna (jenom pár instrukcí). Pro kompilaci používám ML.EXE a ML64.EXE 8.00.50727.42 (dodávané s Visual Studiem 2005).
Příkazové řádky vypadají takto:
ml /c /Cp /Fl /DPFF32 demo.asm link /SUBSYSTEM:WINDOWS /entry:main demo.obj
ml64 /c /Cp /Fl demo.asm link /SUBSYSTEM:WINDOWS /entry:main demo.obj
Instrukce jako mov@
a podobné jsou
makra, která umožňují portabilitu.
; IFDEF PFF32 ; add the header only for 32-bit code .686 .MODEL FLAT, STDCALL ENDIF include pff.asm .DATA? port_base PVOID ? port_index DWORD ? .DATA buffer WORD 5 DUP (20h) .CODE main PROC lea rax, [buffer] ; acts the same in both modes mov@ [port_base], rax mov@ [port_index], 1 mov@ r8, [port_base] mov@ r9d, [port_index] ; (acts as movzx in 64-bit mode) mov@ r11w, 2 add@ [r8+r9*2], r11w mov@ r12w, 22h cmp@ r12w, [r8+r9*2] jne main main ENDP END ;
; IFDEF PFF32 PVOID TYPEDEF DWORD ELSE PVOID TYPEDEF QWORD ENDIF IFDEF PFF32 ; init: no temp register is being used PFF_EBX = 0 PFF_ECX = 0 ; 64-bit general registers (which may hold only a pointer) are simply ; EQUated to 32-bit ones for 32-bit mode rax TEXTEQU <eax> ;... ; usage of any of new general registers causes calling of pff_r macro, ; which move the emulated value into free reserved register (eBX or eCX) r8 EQU <pff_r (r8, d)> r9 EQU <pff_r (r9, d)> ;... r9d EQU <pff_r (r9, d)> ;... r11w EQU <pff_r (r11, w)> r12w EQU <pff_r (r12, w)> ;... ; set registers mapping to reserved registers so it is possible to test ; whether a register is emulated or not rax_mapping TEXTEQU <> ;... r8_mapping TEXTEQU <ebx> r9_mapping TEXTEQU <ebx> ;... r9d_mapping TEXTEQU <ebx> ;... r11w_mapping TEXTEQU <bx> r12w_mapping TEXTEQU <bx> ;... ; Macro pff_get_tmp_r ; ; This macro returns appropriate reserved register, which would be currently ; used with given emulated register ; ; If no reserved register is available, macro returns blank string. ; ; Input: ; regex emulated register name with "_" postfix pff_get_tmp_r MACRO regex:REQ LOCAL postfix IF PFF_EBX AND PFF_ECX EXITM <> ; no reserved register available ENDIF postfix SUBSTR <regex>, @SizeStr (regex) - 1 %IFIDN <postfix>, <d_> ; dword register IFE PFF_EBX EXITM <ebx> ELSE EXITM <ecx> ENDIF %ELSEIFIDN <postfix>, <w_> ; word IFE PFF_EBX EXITM <bx> ELSE EXITM <cx> ENDIF %ELSEIFIDN <postfix>, <b_> ; byte IFE PFF_EBX EXITM <bl> ELSE EXITM <cl> ENDIF ELSE ; qword IFE PFF_EBX EXITM <ebx> ELSE EXITM <ecx> ENDIF ENDIF ENDM ; Macro pff_r ; ; This macro moves the value of an emulated register to free reserved ; register and returns the register. ; ; Input: ; regex emulated register name ; size emulated register size pff_r MACRO regex:REQ, size:REQ IFIDNI <size>, <b> IFE PFF_EBX PFF_EBX = 1 mov bl, pff.global®ex&size EXITM <bl> ELSE PFF_ECX = 1 mov cl, pff.global®ex&size EXITM <cl> ENDIF ELSEIFIDNI <size>, <w> IFE PFF_EBX PFF_EBX = 1 mov bx, pff.global®ex&size EXITM <bx> ELSE PFF_ECX = 1 mov cx, pff.global®ex&size EXITM <cx> ENDIF ELSE IFE PFF_EBX PFF_EBX = 1 mov ebx, pff.global®ex&size EXITM <ebx> ELSE PFF_ECX = 1 mov ecx, pff.global®ex&size EXITM <ecx> ENDIF ENDIF ENDM ; Macro pff_meta ; ; This macro provides the facility for two-operand instructions. ; ; Input: ; type type of operation: read/write ; op the operation itself (mov, add, cmp, test, ...) ; op1 destination operand ; op2 source operand pff_meta MACRO type:REQ, op:REQ, op1:REQ, op2:REQ LOCAL src, dst LOCAL tmp ; add "_" to prevent expansion of possible emulated register IFE @InStr (, op1&_, <[>) ; destination operand is not a memory location ; if the destination is an emulated register... tmp TEXTEQU op1&_mapping %IFNB <tmp> ; if the source is a memory location, load it first to tmp register IF @InStr (, op2&_, <[>) ; source operand is a memory location mov op1&_mapping, op2 PFF_ECX = 0 ; now, the second reserved register can be used ; don't load current emulated value if the operation is MOV IFDIF <op>, <mov> dst TEXTEQU op1 ELSE dst TEXTEQU pff_get_tmp_r (op1&_) ENDIF ; don't perform MOV operation since it is unnecessary in this case ; (MOV is here actually performed by the the former and the latter MOV) IFDIF <op>, <mov> op dst, op1&_mapping ENDIF IFIDN <type>, <write> ; if mov, add, etc., write it back mov [pff.global&op1&], dst ENDIF ELSE ; source operand is not a memory location IFDIF <op>, <mov> dst TEXTEQU op1 ELSE dst TEXTEQU pff_get_tmp_r (op1&_) ENDIF op dst, op2 IFIDN <type>, <write> mov [pff.global&op1&], dst ENDIF ENDIF ELSE op op1, op2 ENDIF ELSE ; op1 is a memory location IFNDEF op2&_mapping ; catch immediate source operand tmp TEXTEQU <> ELSE tmp TEXTEQU op2&_mapping ENDIF %IFNB <tmp> ; source is an emulated register ; if the destination is a memory location, load first its address ; to tmp register IF @InStr (, op1&_, <[>) ; destination operand is a memory location lea ebx, op1 PFF_ECX = 0 ; now, the second reserved register can be used src TEXTEQU op2 ; load current emulated value op [ebx], src ELSE ; destination operand is not a memory location op op1, op2&_mapping ENDIF ELSE op op1, op2 ENDIF ENDIF ; set both temp registers as unused PFF_EBX = 0 PFF_ECX = 0 ENDM ; Macros supplying original instructions mov@ MACRO op1:REQ, op2:REQ pff_meta write, mov, op1, op2 ENDM add@ MACRO op1:REQ, op2:REQ pff_meta write, add, op1, op2 ENDM cmp@ MACRO op1:REQ, op2:REQ pff_meta read, cmp, op1, op2 ENDM ; Internal macro pff_global_r ; ; This internal macro is just used to declare global memory space for ; emulated registers; see PFF struct pff_global_r MACRO regex:REQ UNION global®ex DWORD ? global®ex&d DWORD ? global®ex&w WORD ? global®ex&b BYTE ? ENDS ENDM PFF STRUCT pff_global_r r8 pff_global_r r9 pff_global_r r10 pff_global_r r11 pff_global_r r12 pff_global_r r13 pff_global_r r14 pff_global_r r15 PFF ENDS ELSE ; IF PFF32 mov@ TEXTEQU <mov> add@ TEXTEQU <add> cmp@ TEXTEQU <cmp> ENDIF .DATA? IFDEF PFF32 pff PFF <> ; reserve space for emulated registers ENDIF ;
Výsledný 64bitový kód odpovídá zdrojovému.
main: lea rax, [402000h] mov [402010h], rax mov dword ptr [402018h], 1 mov r8, [402010h] mov r9d, [402018h] mov r11w, 2 add [r8+r9*2], r11w mov r12w, 22h cmp r12w, [r8+r9*2] jne main
Kód je ručně editován, aby byl výsledek jasnější.
main: ; lea rax, [buffer] lea eax, [402000] ; mov@ [port_base], rax mov [402030], eax ; mov@ [port_index], 1 mov dword ptr [402034], 1 ; mov@ r8, [port_base] mov ebx, [402030] mov [402010], ebx ; mov@ r9d, [port_index] mov ebx, [402034] mov [402014], ebx ; mov@ r11w, 2 mov bx, 2 ; unnecessary, don't care mov [40201C], bx ; add@ [r8+r9*2], r11w mov ebx, [402010] mov ecx, [402014] lea ebx, [ebx+ecx*2] mov cx, [40201C] add [ebx], cx ; mov@ r12w, 22h mov bx, 22 ; unnecessary, don't care mov [402020], bx ; cmp@ r12w, [r8+r9*2] mov ebx, [402010] mov ecx, [402014] mov bx, [ebx+ecx*2] mov cx, [402020] ; unnecessary, don't care cmp cx, bx jnz main
pff.asm, makra PFF.
demo.asm, demo PFF.
result64.lst, výsledný listing 64bitového kódu.
result32.lst, výsledný listing 32bitového kódu.
compile64.bat, dávka pro kompilaci do 64bitového kódu.
compile32.bat, dávka pro kompilaci do 32bitového kódu.
x86-64 Tour of Intel Manuals: Shrnutí nových vlastností architektury x64 tak, jak ho podávají Intel manuály (anglicky)
Writing 64-bit programs od Jeremyho Gordona
Microsoft Macro Assembler Reference, MASM for x64 (ml64.exe)
Pokračujte na diskuzním fóru.
2007-04-18 | 1.0 | První veřejná verze | MazeGen |
(formáty dat odpovídají ISO 8601)