dinsdag 24 december 2019

Programmeren in x86-64 assembly met MASM en Visual Studio.

x86-64 is een door AMD ontworpen evolutie van de Intel 8086 en ideaal om innerloops te optimaliseren van een Visual Studio C/C++ programma.

> Registers
Register                Gebruik
rax, eax, ax, ah, al : accumulator (arithmetic operations)
rbx, ebx, bx, bh, bl : base (starting address of data structures)
rcx, ecx, cx, ch, cl : count (loops, bit shift counts)
rdx, edx, dx, dh, dl : data (arithmetic operations)
rsi, esi, si, sil    : source index
rdi, edi, di, dil    : destination index
rbp, ebp, bp, bpl    : basepointer (frame pointer into stack)
rsp, esp, sp, spl    : stackpointer (offset of current top of stack)
r8, r8d, r8w, r8b    : general purpose register.
r9-r15 idem.
float registers: zmm0-zmm15(AVX-512), ymm0-ymm15(AVX) of xmm0-xmm15(SSE)

> Veelgebruikte instructies
   mov rax,0  ; Codebytes: 48 C7 C0 00 00 00 00
En meteen is hier een opmerking te plaatsen. mov eax,0 doet namelijk hetzelfde met 2 codebytes minder.
   mov eax,0  ; Codebytes: B8 00 00 00 00
Je denkt nu: maar eax is een 32 bits register! Maar in x64 genereren 32-bit operatie's een 32-bit resultaat die zero-extended wordt naar 64-bit. mov eax,0 zet dus allemaal nullen in de meest significante 32 bits van het 64-bit register rax. Dit is gedaan omdat anders bij iedere 32-bit instructie gewacht moet worden op de merge met de 32 meest significante bits, terwijl die in de meeste gevallen niet relevant zijn in de code.
   xor eax, eax ; Codebytes: 31 C0

rax en rdx zijn gevaarlijke registers, want met een MUL of DIV instructie worden zij gebruikt om het resultaat in op te slaan.

> Complexe addressing mogelijkheden
mov eax, [rsi+rcx*4+10]
mov eax, [register][register*scalefactor]+displacement
De scalefactor kan 2,4 of 8 zijn. Displacement een 64 bit waarde.

> Segment registers cs(code), ds(data), ss(stack) en es(extra) zijn niet meer interessant. MASM gebruikt het flat memory model bij x64.

> x86-64 is little endian, dat betekent dat "mov qword ptr[r12], 0fedcba9876543210h" wordt opgeslagen in het geheugen als: 10 32 54 76 98 ba dc fe

> Processor flags
CF = carry flag     = bit 0   rotate 'carry out', or overflow/underflow
PF = parity flag    = bit 2   when number of 1's is even in low-order byte of result
AF = auxiliary flag = bit 4   when carry in least sign. 4-bit digit.
ZF = zero flag      = bit 6   when result is zero
SF = sign flag      = bit 7   when result is negative
TF = trap flag      = bit 8   debugging purposes
IF = interrupt flag = bit 9   if 1 then interrupts are responded
DF = direction flag = bit 10  1 = string dir. is towards lower addresses
OF = overflow flag  = bit 11  foutvlag voor getallen met teken

> Processor flags voorwaardelijke jumps.
Meestal doe je eerst een "cmp", bijvoorbeeld "cmp rax, 3". RAX is dan de destination.
ja   spring als destination is hoger             cf=0 en zf=0
jae  spring als destination is hoger of gelijk   cf=0
jb   spring als destination is lager             cf=1
jbe  spring als destination is lager of gelijk   cf=1 of zf=1
je   spring als destination is gelijk            zf=1
*jg  spring als destination is groter            zf=0 en (sf=of)
*jge spring als destination is groter of gelijk  sf=of
*jl  spring als destination is kleiner           sf<>of
*jle spring als destination is kleiner of gelijk zf=1 of (sf<>of)
* = van toepassing op twee-complement
jc   spring als carry gezet       cf=1
jo   spring als overflow gezet    of=1
js   spring als negatief          sf=1
jz   spring als nul               zf=1
al deze instructie's kunnen ook negatief gebruikt worden, b.v. jnge of jns
special: jecxz/jrcxz spring als ecx/rcx is zero.

> Cache
CPU register: 1 clockcycle
L1 Cache: 3 clockcycles.  (64 bytes per cacheline)
L2 Cache: 15 clockcycles.
L3 Cache: 60 clockcycles.
DRAM: 150 clockcycles.
Conclusie: Probeer het raadplegen van geheugen dus zoveel mogelijk sequentieel(horizontaal) te doen.

> Size naamgeving
MOVSQ verplaatst 8 bytes. De naam "quad" doet je denken dat je 4 bytes verplaatst. Dat klopt dus niet, het staat voor quad words. 4 * 2 bytes = 8 bytes = 64 bits. MOVSD vervplaatst een double word, dus 32 bits = 4 bytes. MOVSW verplaatst 16 bits = 2 bytes.

> Interne CPU optimalisatie
Een "rep movsb" is even snel als "rep movsq" sinds Haswell (4770). "Rep movsb" gebruikt intern 256-bit.
Draai je de volgende code:
cvtsi2ss xmm4, rcx
cvtsi2ss xmm5, r11
divss xmm4, xmm5
movss xmm4, real4 ptr [rcx*4+r10]  ; precalculated value in xmm4
... dan voert de CPU de divss mnemonic niet uit. Slim!


> Assembler toevoegen in een Visual Studio C/C++ programma
Als je een nieuw C++ project aanmaakt in Visual Studio dan kun je de rechtermuis knop gebruiken op het project. Je kunt dan onder "Build Dependencies" de optie "Build Customizations" selecteren. Vink in de dialog "masm (.targets, .props)" aan. Vervolgens kun je .asm files gebruiken in je C/C++ programma.


> Aanroepen van MASM functie's vanuit C/C++
Een functie die in MASM gedefinieerd is kun je in C/C++ declareren op de volgende manier:
extern "C" void ASM_proc(void* par1, Sint64 par2);

Variabelen kun je in C/C++ zo definieeren:
extern "C" void* objVoidP = NULL;
extern "C" Sint64 objSInt64 = NULL;

Vervolgens kun je in de MASM file op de volgende manier naar de variabelen verwijzen:
extern objVoidP: qword
extern objSInt64: qword
(een externe functie kun je in MASM definieeren met "extern remoteProc: proc")

Registers met waarden die je niet hoeft te behouden zijn: rax, rcx, rdx, r8-r11. Andere registers moet je herstellen als je de functie verlaat.
xmm0-xmm5(including) hoef je ook niet te behouden.

Extern "C" aanroep conventie:
parameter1 = rcx/xmm0
parameter2 = rdx/xmm1
parameter3 = r8/xmm2
parameter4 = r9/xmm3
Bij een mix van integers en floats worden registers overgeslagen. Voorbeeld: func3(int a, double b, int c, float d); ->  a in RCX, b in XMM1, c in R8, d in XMM3
De rest van de parameters worden op de stack opgeslagen.


> MASM assembler file layout

extern main_pointer1: qword

.data
align 16
tmpfloat real4 201.0
tmpdouble real8 402.0

.code

ASM_proc proc
 push rbp
 mov rbp, rsp
 sub rsp, 20h        ; 4 qwords space for the 4 parameters.
 mov [rbp-8h], rcx   ; local storage of par1
 mov [rbp-10h], rdx  ; local storage of par2
 mov [rbp-18h], r8   ; local storage of par3
 mov [rbp-20h], r9   ; local storage of par4
 mov rax, [rbp+30h]  ; parameter 5
 mov rax, [rbp+38h]  ; parameter 6
 
    push rdi
    mov rdi, main_pointer1
    mov eax, 3
    cmp eax, 04h
    jnc @F              ; @B voor de voorgaande @@
 mov 
    mov eax, -1
@@:
    pop rdi 
 
 mov rsp, rbp
 pop rbp
 ret
ASM_proc endp

end


> MASM datatypes
    BYTE - 8 bit unsigned integer
    SBYTE - 8 bit signed integer
    WORD - 16 bit unsigned integer
    SWORD - 16 bit signed integer
    DWORD - 32 bit unsigned integer
    SDWORD - 32 bit signed integer
    FWORD - 48 bit integer
    QWORD - 64 bit integer
    TBYTE - 80 bit (10 byte) integer
    REAL4 - 32 bit (4 byte) short real
    REAL8 - 64 bit (8 byte) long real
    REAL10 - 80 bit (10 byte) extended real

> MASM
eerste_byte   equ   this byte
woord_tabel   dw    100 DUP(?)
creeert eerste_byte en geeft deze een byte-attribuut met hetzelfde adres als
woord_tabel. hetzelfde kan bereikt worden met:
eerste_byte   equ   byte ptr woord_tabel

- mov [rbx],0 laat niet weten of een byte, word, dword of qword in [rbx] moet worden
  geplaatst. Doe dat met mov dword ptr [rbx], 0

- hexadecimale getallen worden weergegeven met eerst een nul en op het
  eind een h. dus b.v. 02f35h