Portabilní flat syntaxe x86

MazeGen, 2007-04-18Revize: 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).

Novinky na architektuře x64

Uveďme si stručně hlavní novinky na architektuře x64:

  1. RIP-relative a EIP-relative adresace (příklad: MOV [RIP-2008h], RAX)
  2. Operandy rozšířené na 64 bitů
  3. Osm nových všeobecných registrů, r8r15
  4. Osm nových XMM registrů, xmm8xmm15

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:

Jak je vidět, při návrhu nové společné syntaxe se nejde vyhnout zavedení určitých základních pravidel.

Minimální PFS

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:

Návrh konečných pravidel pro PFS

  1. RIP-relative a EIP-relative adresace

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é.

  1. Operandy rozšířené na 64 bitů

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

  1. Osm nových všeobecných registrů, r8r15
  2. Osm nových XMM registrů, xmm8xmm15

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.

Pravidla PFS

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:

  1. Nelze používat 64bitové registry. Výjimkou je použití bázového registru, který musí být vždy zapisován jako 64bitový registr. V případě indexu nesmí jeho vynásobená hodnota nikdy přesáhnout FFFFFFFFh, jinak by nebyla hodnota v 32bitovém kódu dostupná.
  2. Pro uložení ukazatele do paměti musí být použit zvláštní typ PVOID, který zajistí portabilitu ukazatele.
  3. Nelze explicitně používat RIP-relative adresaci, která nemá v 32bitovém kódu náhradu. Na úrovni generace kódu asemblerem tato adresace být použita může.
  4. Nelze explicitně používat instrukce PUSH a POP. Jedinou výjimkou je instrukce PUSH const, kde const je nižší nebo rovna FFFFFFFFh.
  5. Lze používat všechny registry (i nové). Výjimkou je omezené použití eBX, eCX a xmm7 (viz následující pravidlo).
  6. Pro transformaci do 32bitového kódu platí: Pokud je použit některý z nových všeobecných registrů ať už přímo, nebo jako bázový registr v adrese, je přepsán registr 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.
  7. Vlákna jsou vytvářena pomocí prostředků implementace této syntaxe. Tím je ošetřena emulace nových registrů v multithreadovém prostředí.

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

Portabilní flat framework x86 (PFF)

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

PFS demo

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

;

Makra PFF

;

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&regex&size
     EXITM <bl>
   ELSE
     PFF_ECX = 1
     mov cl, pff.global&regex&size
     EXITM <cl>
   ENDIF

 ELSEIFIDNI <size>, <w>
   IFE PFF_EBX
     PFF_EBX = 1
     mov bx, pff.global&regex&size
     EXITM <bx>
   ELSE
     PFF_ECX = 1
     mov cx, pff.global&regex&size
     EXITM <cx>
   ENDIF

 ELSE
   IFE PFF_EBX
     PFF_EBX = 1
     mov ebx, pff.global&regex&size
     EXITM <ebx>
   ELSE
     PFF_ECX = 1
     mov ecx, pff.global&regex&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&regex     DWORD ?
  global&regex&d   DWORD ?
  global&regex&w   WORD ?
  global&regex&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

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

Výsledný 32bitový kód

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

Download

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.

Odkazy

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)


Komentáře

Pokračujte na diskuzním fóru.

Můj kontakt naleznete zde.


Revize

2007-04-181.0První veřejná verze MazeGen

(formáty dat odpovídají ISO 8601)