x86-64汇编学习#

之前在编译器课程中做了一些实战,用的mips指令。
另外研究xv6时也折腾过一些riscv汇编。
可以进一步综合研究一下,做一个能实战的C编译器。
需要先把x86-64汇编相关知识快速学习更新一下,把各种概念/流程/环境理清。

主要跟这本书来学
<<The Art of 64-bit Assembly Language>> VOLUME 1

代码在 https://artofasm.randallhyde.com/

讲得非常详细。可先跳过一些章节,以后碰到再更新。


1 hello world#

  • intel i9-13900K

  • windows 11

  • visual studio 2022

  • MASM Microsoft Macro Assembler reference
    https://learn.microsoft.com/en-us/cpp/assembler/masm/microsoft-macro-assembler-reference?view=msvc-170

1.1 需要准备什么#

安装visual studio,包括c/c++等组件。

1.2 设置masm#

安装visual studio,勾上c/c++相关组件即可。

如果只用命令行,不需要特别的设置。


ide设置
https://www.youtube.com/watch?v=zbOuzJkk4Fs

vs2022中设置项目

  1. new peoject选c++ console app

  2. 默认的cpp代码替换为本书的c.cpp代码

  3. source files里添加item,选cpp文件,后缀改为asm即可。填入你的asm代码。

  4. 左边explorer里右击项目名,选build dependencies/build customizations,勾上masm。

  5. 右击.asm文件,选properties,item type选microsoft macro assembler。

即可编译运行

asm中可打断点,debug->windows里可查看寄存器值,memory等。


我在5.2节才开始用vs2022。
因为发现bat脚本和vs2022运行有差异。
估计编译参数有区别?暂时忽略。

1.3 文本编辑器#

自备

1.4 MASM程序#

; Comments consist of all text from a
; semicolon character to the end of the line.

; The ".CODE" directive tells MASM that the
; statements following this directive go in
; the section of memory reserved for machine
; instructions (code).

        .CODE

; Here is the "main" function.
; (This example assumes that the
; assembly language program is a
; stand-alone program with its own
; main function.)

main    PROC

; << Machine Instructions go here >>
        
        ret ;Returns to caller
    
main    ENDP

; The END directive marks the end of
; the source file.

        END

1.5 运行第一个MASM程序#

ml64.exe是masm的assembler

运行ml64.exe
"c:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.37.32822\bin\Hostx64\x64\ml64.exe" programShell.asm  /link /subsystem:console /entry:main

可在开始菜单中找x64 native tools Command Prompt For VS2022
在里面可直接运行ml64和cl等工具。

E:\dev\asm\test>ml64 programShell.asm /link /subsystem:console /entry:main
Microsoft (R) Macro Assembler (x64) Version 14.37.32824.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: programShell.asm
Microsoft (R) Incremental Linker Version 14.37.32824.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:programShell.exe
programShell.obj
/subsystem:console
/entry:main

1.6 c++和asm混合编译#

public使得asm的函数可以被外部调用

cl相关
cl是c++编译工具
https://learn.microsoft.com/en-us/cpp/build/reference/compiling-a-c-cpp-program?view=msvc-170

编译/链接
ml64 /c listing1-3.asm
cl listing1-2.cpp listing1-3.obj

运行
listing1-2.exe

1.7 x86-64 cpu家族#

到底啥是x86/x64/x86-64

https://en.wikipedia.org/wiki/X86-64

https://phoenixnap.com/kb/x64-vs-x86

https://www.seeedstudio.com/blog/2020/02/24/what-is-x86-architecture-and-its-difference-between-x64/

冯诺依曼体系

有一堆general-purpose寄存器,有叠加关系。

有一堆specialpurpose寄存器,包括8个浮点寄存器。

RFLAGS寄存器。包含一些独特的标志位。

1.8 memory子系统#

byte-addressable memory

数据怎么排列

1.9 MASM中声明内存变量#

memory variables

在.data后声明memory变量。

例如

.data

wtf byte 6

ggg byte ?

i8 sbyte 250

; Zero-terminated C/C++ string.
strVarName byte 'String of characters', 0

, 0是追加数据

不用关心声明的变量的地址,MASM会管理好。

1.10 constant#

wtf = 256

wtffff equ 256

常量可放在代码的任意位置。

1.11 基本的机器指令#

intel的指令文档
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

https://cdrdv2.intel.com/v1/dl/getContent/671110

MASM语言相关文档
https://learn.microsoft.com/en-us/cpp/assembler/masm/directives-reference?view=msvc-170

x86-64 cpu提供成百上千个机器指令。
但是一般汇编只用到30-50个。

mov     rdx, i

mov不允许两个操作对象都为内存变量

两个操作对象的size有限制,见table 1-5。

add     rdx, 12
sub     rdx, 12

光mov/add/sub三个指令就可以写出比较复杂的程序。

lea reg64, memory_var

lea
load effective address
把变量的地址load到寄存器

strVar byte "Some String", 0

...

lea rcx, strVar

函数调用相关

proc起函数定义 ret返回 endp定义结束

; Listing 1-4
; A simple demonstration of a user-defined procedure.

        .code

; A sample user-defined procedure that this program can call.

myProc  proc
        ret    ; Immediately return to the caller
myProc  endp

; Here is the "main" procedure.

main    PROC

; Call the user-define procedure

        call   myProc

        ret     ;Returns to caller

main    ENDP
        END

1.12 汇编调用c/c++函数#

汇编可以调用外部的c/c++函数。例如用printf来打印信息。

.code中externdef printf:proc声明外部函数

call printf之前要设置参数,会有一些规则。
lea rcx, fmtStr把字符串地址放到rcx。如果有参数,依次放到rdx,r8,r9。
printf默认打印rcx中地址上的数据。

第五章详细讲函数调用相关。

1.13 函数例子#

; Listing 1-5
;
; A "Hello, World!" program using the C/C++ printf function to 
; provide the output.

        option  casemap:none
        .data

; Note: "10" value is a line feed character, also known as the
; "C" newline character.
 
fmtStr  byte    'Hello, World!', 10, 0

        .code

; External declaration so MASM knows about the C/C++ printf 
; function

        externdef   printf:proc

        
; Here is the "asmFunc" function.

        
        public  asmFunc
asmFunc proc

; "Magic" instruction offered without explanation at this 
; point:

        sub     rsp, 56

; Here's where will call the C printf function to print 
; "Hello, World!" Pass the address of the format string
; to printf in the RCX register. Use the LEA instruction 
; to get the address of fmtStr.
        
        lea     rcx, fmtStr
        call    printf
 
; Another "magic" instruction that undoes the effect of the 
; previous one before this procedure returns to its caller.
       
        add     rsp, 56
        
        ret     ;Returns to caller
        
asmFunc endp
        end

1.14 返回值#

如何返回数据涉及到caller和callee之间如何协商。
如果不定规则,大家都随心所欲,那么合作起来就是灾难。

这个协商叫做ABI(application binary interface)。
定义具体的calling convention(数据往哪传,从哪回,寄存器怎么安排)、数据类型、内存使用等等细节。

cpu厂商、编译器厂商、操作系统等等都会规定一套自身的ABI。
我们使用Microsoft Windows ABI。
https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions?view=msvc-170

Windows ABI规定函数返回值放在rax。


看懂c.cpplisting1-8.asm

cl参数列表
https://learn.microsoft.com/en-us/cpp/build/reference/compiler-options-listed-by-category?view=msvc-170

ml64参数列表
https://learn.microsoft.com/en-us/cpp/assembler/masm/ml-and-ml64-command-line-reference?view=msvc-170

1.15 自动化编译#

熟悉更多的编译参数

可做一个.bat自动输入繁琐的参数进行编译。

c.cpp作为入口,提供printf,调用汇编的入口asmMain
汇编调用printf打印信息。

1.16 Microsoft ABI#

我们会相互调用各种库和代码,ABI的兼容非常重要。

包括变量的size、寄存器的使用、栈的对齐等方面。

具体看文档
https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions?view=msvc-170

1.17 其他信息#

其他资料链接

2 计算机数据的表示和操作#

2.1 数字系统#

介绍十进制和二进制

2.2 十六进制#

介绍十六进制

2.3 数字vs表示#

一个数的值是一定的。xx进制只是其表示方法。

2.4 数据组织#

bits

nibbles

bytes

LO-low order
HO-high order

words

double words

quad words

octal words

2.5 单个bit的逻辑操作#

2.6 bits的操作以及对应的汇编指令#

and dest, source 
or  dest, source 
xor dest, source

not dest

xor reg, reg

看懂例子代码

2.7 有符号和无符号数#

x86-64用二的补码表示有符号数。

看懂例子代码。

2.8 Sign Extension和Zero Extension#

不同size的数据互转

2.9 Sign Contraction和Saturation#

2.10 跳转#

jmp statement_label

cmp left_operand, right_operand

jc
jnc
jo
jno
...

各种跳转

跳转有同义词

2.11 Shifts和Rotates#

shl dest, count
shr dest, count
sar dest, count
rol dest, count 
ror dest, count
rcl dest, count 
rcr dest, count

2.12 Bit Fields和Packed Data#

有时候需要使用8/16/32/64之外的数据长度。

2.13 IEEE浮点数#

2.14 BCD#

2.15 字符#

2.16 unicode#

2.17 MASM的unicode#

2.18 其他信息#

3 内存的使用和管理#

3.1 运行时内存管理#

  • code
    具体的机器指令即汇编代码

  • uninitialized static data 一片未初始化的内存区域,生命周期是整个程序。

  • initialized static data
    一片已经初始化的内存区域

  • read-only data
    只读。一般存常量

  • heap
    用来存放动态分配的内存

  • stack
    主要存放local变量之类的短期数据

不同数据类型在内存中有自己的摆放位置,见图3-1。

回忆xv6系统也是一样,进程会有自己的内存布局。


.code里是机器指令。MASM把这些指令转成cpu能识别的数据喂给cpu。

.data中放变量

.const放常量、表等数据。比如定义pi和e。

.data?中存未初始化的变量。windows会初始化为0。exe文件会更小。

他们的顺序没有规定,可随意出现。

.data 
i_static sdword 0 

.data? 
i_uninit sdword ? 

.const 
i_readonly dword 5

.data 
j dword ? 

.const 
i2 dword 9 

.data? 
c byte ? 

.data? 
d dword ? 

.code 
    Code goes here 
    
    end

x86-64的mmu把内存按page分割,和riscv一样。

按页管理,每页有自己的属性和状态。
对其读写产生相应的后果。

3.2 MASM如何为变量分配内存#

某个section(.code, .data, .const, and .data?)中的变量按顺序排在某个实际的内存地址。

不同section可能从不同地址开始排,section之间不是顺序贴着的。

3.3 label的定义#

.const 

abcd label dword 
    byte 'a', 'b', 'c', 'd'
    

label本身不会占用内存,而是共用它之后的object的地址。

3.4 大小端#

x86-64是小端
把低位字节放在低位地址,高位字节放在高位地址。

xchg交换数据

3.5 Memory Access#

cpu如何读内存数据

不同bus大小的情况
没对齐的地址的情况

cpu cycle

不对齐可能浪费cpu cycle

3.6 MASM中的数据对齐#

要想程序跑得快,必须关注对齐。

不同变量声明顺序会形成不同的内存布局。

很难每次通过精细地安排声明顺序来做对齐。
可用MASM的align来对齐。

align 强制下一个变量分配在对齐的地址上。

align为了对齐可能插入一些填充字节。空间换时间。

对于现代x86-64cpu,对齐可能不是必须,cpu有办法自己处理。

3.7 x86-64寻址模式#

目前为止,我们只知道一种方式来操作变量,PC-relative。

  1. Register Addressing Mode
    直接操作寄存器
    mov ax, bx ;

  2. PC-Relative Addressing Mode
    最常用易懂。mov al, symbol ;
    以rip寄存器为基准,获取某个symbol的值。所以也叫作RIP-relative。

  3. Register-Indirect Addressing Mode
    Indirect意思是不直接使用寄存器的值,而是把寄存器的值当作地址,去操作这个地址上的数据。 必须为64位寄存器。
    mov [reg64], al

  4. Indirect-Plus-Offset Addressing Mode
    可对寄存器值加减作为地址
    mov [reg64 + constant], source

  5. Scaled-Indexed Addressing Mode
    mov al, [rbx + rsi*4 + 2000h]
    常用于数组相关

大地址问题
64位寻址的一个优势是地址范围能达到8TB。

3.8 地址的表示#

3.9 栈操作#

通过rsp(stack pointer)寄存器操纵栈。

push reg16 
push reg64 
push memory16 
push memory64 
pushw constant16 
push constant32 ; Sign extends constant32 to 64 bits
pop reg16 
pop reg64 
pop memory16 
pop memory64

pop后原地址的数据不会被清掉。只是逻辑上不可用。

push/pop可以很方便地进行寄存器值的临时存取。

3.10 LIFO#

last in first out

栈操作必须完美,错一点就全完。

3.11 其他push/pop指令#

pushf pushfq popf popfq

3.12 不用pop的条件下完成出栈#

直接改rsp

3.13 不用pop的条件下操作数据#

直接用地址取值
mov rax, [rsp + 8]

3.14 Microsoft ABI相关#

3.15 其他信息#

4 常量/变量/数据类型#

4.1 imul#

4.2 inc/dec#

inc mem/reg 
dec mem/reg

4.3 MASM常量的声明#

代码中可多次声明。

maxSize = 100 
Code that uses maxSize, expecting it to be 100 

maxSize = 256 
Code that uses maxSize, expecting it to be 256

可以是复杂的表达式

identifier = constant_expression 
identifier equ constant_expression

constant_expression可包含一系列的操作符。

this和$
返回当前指令的offset。可用来实现一些小功能,比如算size等等。

hwStr       byte    "Hello World!"
hwLen       =       $-hwStr

4.4 MASM的typedef#

new_type_name typedef existing_type_name

integer typedef sdword 
float typedef real4 
double typedef real8 
colors typedef byte

.data 
i integer ? 
x float 1.0 
HouseColor colors ?

4.5 Type Coercion/类型强转?#

一些操作比如mov,需要两边size一致。把8bit数据mov到32bit寄存器会报错。

有时候需要确实需要这么做,可以用Type coercion,强行操作。

new_type_name ptr address_expression

;把byte_values地址上的值当作word存到ax
mov ax, word ptr byte_values

4.6 指针#

其实就是把一个64bit的qword变量当作一个指针,然后折腾各种地址相关问题。

.data

pointer typedef qword

b byte ? 
d dword ? 
pByteVar pointer b 
pDWordVar pointer d

4.7 组合类型#

4.8 字符串#

4.9 数组#

.data

; Character array with elements 0 to 127. 
CharArray byte 128 dup (?)

; Array of bytes with elements 0 to 9.
ByteArray byte 10 dup (?)

; Array of double words with elements 0 to 3.
DWArray dword 4 dup (?)

RealArray real4 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
IntegerAry sdword 1, 1, 1, 1, 1, 1, 1, 1

RealArray real4 8 dup (1.0)
IntegerAry sdword 8 dup (1)

RealArray real4 4 dup (1.0, 2.0)
IntegerAry sdword 4 dup (1, 2)

4.10 多维数组#

4.11 Records/Structs#

student struct 
sName byte 65 dup (?) ; "Name" is a MASM reserved word Major word ? 
SSN byte 12 dup (?) 
Midterm1 word ? 
Midterm2 word ? 
Final word ? 
Homework word ? 
Projects word ? 
student ends

.data 
John student {}

如何操作成员的值

mov rcx, sizeof student ; Size of student struct 
call malloc ; Returns pointer in RAX 
mov [rax].student.Final, 100

可以嵌套

grades struct 
Midterm1 word ? 
Midterm2 word ? 
Final word ? 
Homework word ? 
Projects word ? 
grades ends 

student struct 
sName byte 65 dup (?) ; "Name" is a MASM reserved word 
Major word ? 
SSN byte 12 dup (?) 
sGrades grades {} 
student ends

初始化
数据放在{}里
{}里的元素要和定义对的上

strDesc struct 
maxLen dword ? 
len dword ? 
strPtr qword ? 
strDesc ends

aString strDesc {len, len, offset charData}

struct的数组

4.12 Unions#

4.13 ABI相关#

4.14 其他信息#

5 PROCEDURES#

5.1 procedure的实现#

Procedure中文到底应该叫啥。我就叫函数了,知道意思就行。

call printf用call来call函数

;定义

proc_name proc options 
    Procedure statements 
proc_name endp

看懂程序5-1

; Listing 5-1
;
; Simple procedure call example.


         option  casemap:none

nl       =       10

         .const
;起title字符串
ttlStr   byte    "Listing 5-1", 0

 
        .data
;起buffer。4byte x 256
dwArray dword   256 dup (1)

        
        .code

; Return program title to C++ program:

         public getTitle
getTitle proc
;title地址load到rax
         lea rax, ttlStr
         ret
getTitle endp



; Here is the user-written procedure
; that zeros out a buffer.

;rcx为buffer地址。循环往256个地址分别写4字节0。实现buffer清零。  
zeroBytes proc
          mov eax, 0
          mov edx, 256
repeatlp: mov [rcx+rdx*4-4], eax
          dec rdx
          jnz repeatlp
          ret
zeroBytes endp


; Here is the "asmMain" function.

        public  asmMain
asmMain proc

; "Magic" instruction offered without
; explanation at this point:

        sub     rsp, 48

;填参数,调函数。
        lea     rcx, dwArray
        call    zeroBytes 

        add     rsp, 48 ;Restore RSP
        ret     ;Returns to caller
asmMain endp
        end

call做两件事

  1. 把call之后的第一个指令的64bit地址(即返回地址)入栈

  2. 调转到函数的地址开始执行

ret弹出返回地址,继续执行代码。

如果忘写ret,会直接继续往下跑代码。一般是严重问题。

5.2 保存机器的状态#

程序5-3想演示死循环
但是我的机器并没有出现死循环,只是打印了第一次循环就终止。
应该是出现了异常,没有打印cpp中的terminated。

程序5-4想演示修复死循环 但我运行时只是打印了第一次循环就终止。

估计是abi相关有啥问题。

书中也有笔误,把rbx写成了rcx。

各种测试,没找到规律。在printf之前push一个寄存器就会让printf卡住,程序不能正常结束。

不知道原因。先往后看。


逻辑没问题,无非就是函数里面的循环把外面的寄存器值给误写了,进函数后保存一下就能解决,返回时恢复现场。

被调用者(callee)保存数据的两个好处,空间和可维护性。

一般约定callee负责保存好自己会修改的寄存器。


后记  

到此我都是用书中的build脚本编译运行。  
想着用debug工具看看能不能查出问题。然后在vs2022中起项目,结果vs2022中运行都是符合预期的。  
非常坑,估计是编译参数的差异造成问题?暂时忽略。  


这个问题,在print40Spaces前后push/pop rbx即可解决。  

5.3 函数和stack#

ret无脑弹出栈顶数据,当作地址跳过去执行。
所以必须精确维护好栈数据。保证ret时栈顶是返回地址。否则程序跑飞。

当调用函数时,程序会把相关数据组织在一个Activation Record里。
包括返回地址,参数怎么摆放,函数中的变量怎么摆放等等。

之前的编译器课程中已经见过,我们需要想办法把这些数据妥善安排在栈上。
包括各种格式和顺序。
可能需要规划每个函数的栈大小。

具体就要看各家abi的规定了。


有一套标准的调函数流程Standard Entry Sequence

push rbp ; Save a copy of the old RBP value 
mov rbp, rsp ; Get ptr to activation record into RBP 
sub rsp, num_vars ; Allocate local variable storage plus padding

caller先负责把参数入栈。 进入函数后,函数把rbp保存一下,写为rsp的值。
上面是已经放好的返回地址和参数。
下面自己安排local变量。

然后rbp就是此函数栈的base地址。

num_vars到底是多少,得非常明确。
对于Microsoft ABI代码,会在call之前摆放32字节的4个参数(shadow storage),并保证16字节对齐。
然后进入函数,会立即放一个8字节的返回地址。那么就造成不是16字节对齐。

如果后续不需要任何空间,那么可以直接sub rsp, 8。

如果后续还要用更多内存,比如16字节的local变量,那么减8+16=24。

如果你还要调用别的函数,那么按照Microsoft ABI,你得负责强制开辟4个参数的空间32字节。那么就是减8+16+32=56。

如果不清楚当前的对齐情况,可以and rsp, -16强行对齐。


有一套标准的函数返回流程Standard Exit Sequence

mov rsp, rbp ; Deallocate locals and clean up stack 
pop rbp ; Restore pointer to caller's activation record 
ret ; Return to the caller

清空栈,rsp恢复初始值。
pop,恢复rbp,此时rsp指向返回地址所在的地址。
ret,跳转到返回地址。

Microsoft ABI中caller负责清除参数。
如果需要callee自己清除,可以ret parm_bytes,返回连带pop参数的字节数。

leave指令,简化返回的日常操作。
等同于

mov rsp, rbp 
pop rbp

上面的Standard Exit Sequence就变成

leave
ret

5.4 local变量#

local变量的两种属性:scopelife time

作用域是编译时属性
生命周期是运行时属性

回忆编译器课程,这一块的逻辑花了大力气。

mov [rbp-4], ecx ; a = ECX 
mov [rbp-8], edx ; b = EDX

local变量a排在rbp-4,b排在rbp-8。

这样很麻烦。可以直接定义。

a equ <[rbp-4]> 
b equ <[rbp-8]>

这样容易出错。可以使用masm的local
不用折腾各种offset。masm会帮你做好offset。

identifier:type 
identifier[elements]:type
procWithLocals proc 
    local var1:byte, local2:word, dVar:dword
    local qArray[4]:qword, rlocal:real4
    local ptrVar:qword
    local userTypeVar:userType
procWithLocals endp

最终的布局。会做对齐。

var1 . . . . . . . . . . . . . byte  rbp - 00000001 
local2 . . . . . . . . . . . . word  rbp - 00000004 
dVar . . . . . . . . . . . . . dword rbp - 00000008 
qArray . . . . . . . . . . . . qword rbp - 00000028 
rlocal . . . . . . . . . . . . dword rbp - 0000002C 
ptrVar . . . . . . . . . . . . qword rbp - 00000034 
userTypeVar  . . . . . . . . . qword rbp - 0000003C

prologueepilogue选项可帮助生成Standard Entry Sequence/Standard Exit Sequence代码。
具体待研究

5.5 参数#

pass by value callee不会改变此参数

pass by reference 可以用offset获取地址,有一些问题。
lea更好。


可以用寄存器传参数

传一个参数
Byte        CL 
Word        CX 
Double word ECX 
Quad word   RCX


传多个参数
First                                           Last 
RCX, RDX, R8, R9, R10, R11, RAX, XMM0/YMM0-XMM5/YMM5

一般情况下,用gp寄存器传int和非浮点值。
浮点值用XMMx/YMMx寄存器。

程序5-10演示用rdi和al传参数。


可以通过call的规则,把call之后第一个指令当作参数。

call print 
byte "This parameter is in the code stream.",0

因为call之后的第一个指令就是call的返回地址,那么在函数里直接可以从返回地址取参数。

感觉是一种trick。没仔细看。


通过stack传参数最常用。
寄存器毕竟有限。

caller把参数反着入栈,就形成了之前看到的内存布局。

;CallProc(i, j, k)

push k ; Assumes i, j, and k are all 32-bit 
push j ; variables 
push i 
call CallProc

x86-64 64-bit CPU,必须push64bit值。

关于参数的push还有一系列说法,待研究。


取栈上的value参数

可以像之前定义local变量一样搞。

theParm equ <[rbp+16]>

从rbp往上取即可。


masm提供了声明参数的方法。

procWithParms proc k:byte, j:word, i:dword 
    . 
    . 
    . 
procWithParms endp

https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170#calling-convention-defaults
shadow storage
Microsoft ABI默认自动会把RCX/RDX/R8/R9的值当作四个参数放到栈上。
像ijk,从rbp+16开始。


取栈上的reference参数

看程序5-13

5.6 Calling Convention和Microsoft ABI#

32bit程序时代,不同语言之间无法相互调函数。
intel等厂商出了一套协议即ABI解决这个问题。

一些历史

5.7 Microsoft ABI和Microsoft Calling Convention#

正式定义Microsoft Calling Convention

5.7.1 数据类型#

见table 1-6

基本数据是1/2/4/8字节。

所有参数都是64bit。

如果需要大于64bit的参数,Microsoft ABI要求以引用的形式来传。

5.7.2 参数位置#

参数编号

如果是scalar/reference

如果是浮点数

1

rcx

xmm0

2

rdx

xmm1

3

r8

xmm2

4

r9

xmm3

5->n

on stack 从左往右

on stack 从右往左

void someFunc(int a, double b, char *c, double d)
这样一个声明会用到rcx,xmm1,r8,xmm3。

5.7.3 Volatile/Nonvolatile寄存器#

按Microsoft ABI的要求
Volatile意思是函数可以不保存该寄存器,而对其随意更改。
Nonvolatile要求函数必须维护该寄存器的原始值,否则可能出问题。

5.7.4 栈对齐#

Microsoft ABI规定调函数时,需要栈按16字节对齐。

当windows或其他Windows ABI代码执行你的asm代码时,会保证stack是8字节对齐。

如果你调用Microsoft ABI函数,你得先确保stack对齐。

两种方法

  1. 在你的函数里小心翼翼地维护好rsp

  2. 在call之前强行对齐

and rsp, -16

-16即二进制1…10000。and把rsp最后4bit清零,即实现对齐。

因为清零是一个减法得效果,是把rsp往下挪动,不会损坏现有数据。

5.7.5 参数的设置和清理#

Microsoft ABI函数的一般形式

; Make room for parameters. parm_size is a constant ; with the number of bytes of parameters required ; (including 32 bytes for the shadow parameters). 

sub rsp, parm_size ;开辟空间

Code that copies parameters to the stack 摆放参数 

call procedure ;调用

; Clean up the stack after the call: 

add rsp, parm_size ;加rsp,收回空间。

这样搞有两个问题:

  1. 每次调函数都得add/sub

  2. 之前看到的,对齐操作会把rsp减掉一个未知的数。这样会搞不清楚这里应该add/sub多少值。

如果需要调用多个函数,可以把多个函数放在一起,头尾做一次add/sub即可。

5.8 Functions and Function Results#

按严格的定义,procedure和function是两个东西。
funtion有返回数据。

我这不管了,都叫函数。

5.9 递归#

Recursive proc 
    call Recursive 
    ret 
Recursive endp

qsort演示递归

5.10 函数指针#

x86-64三种call

call proc_name ; Direct call to procedure proc_name 

call reg64 ; Indirect call to procedure whose address ; appears in the reg64 

call qwordVar ; Indirect call to the procedure whose address ; appears in the qwordVar quad-word variable

5.11 函数作为参数#

procWithProcParm proc parm1:word, procParm:proc

call procParm

5.12 保存机器的状态2#

5.13 Microsoft ABI相关#

5.14 其他信息#

6 ARITHMETIC#

6.1 x86-64整数运算#

6.2 运算表达式#

6.3 逻辑表达式#

6.4#

7 low-level控制结构#

7.1 Statement Labels#

label被大量使用。对label可做三件事

  1. jump跳转执行

  2. call一个label

  3. 获取label的地址

mov rcx, offset label1

lea rax, label2

proc/endp对中定义的label是local的,只在该函数中可见。

;可用选项进行相关设置
option noscoped 
option scoped

7.2 jmp#

jmp label 
jmp reg64 
jmp mem64

好好看下程序7-4,是一个较为完整的小程序。

; Listing 7-4
;
; Demonstration of register indirect jumps

        option  casemap:none

nl          =       10
maxLen      =       256
EINVAL      =       22      ;"Magic" C stdlib constant, invalid argument
ERANGE      =       34      ;Value out of range


            .const
ttlStr      byte    "Listing 7-4", 0
fmtStr1     byte    "Enter an integer value between "
            byte    "1 and 10 (0 to quit): ", 0
            
badInpStr   byte    "There was an error in readLine "
            byte    "(ctrl-Z pressed?)", nl, 0
            
invalidStr  byte    "The input string was not a proper number"
            byte    nl, 0
            
rangeStr    byte    "The input value was outside the "
            byte    "range 1-10", nl, 0
            
unknownStr  byte    "The was a problem with strToInt "
            byte    "(unknown error)", nl, 0
            
goodStr     byte    "The input value was %d", nl, 0

fmtStr      byte    "result:%d, errno:%d", nl, 0

            .data
            externdef _errno:dword  ;Error return by C code
endStr      qword   ?
inputValue  dword   ?
buffer      byte    maxLen dup (?)
        
            .code
            externdef readLine:proc
            externdef strtol:proc
            externdef printf:proc
            
; Return program title to C++ program:

            public  getTitle
getTitle    proc
            lea     rax, ttlStr
            ret
getTitle    endp


; strToInt-
;
;  Converts a string to an integer, checking for errors.
;
; Argument:
;    RCX-   Pointer to string containing (only) decimal 
;           digits to convert to an integer.
;
; Returns:
;    RAX-   Integer value if conversion was successful.
;    RCX-   Conversion state. One of the following:
;           0- Conversion successful
;           1- Illegal characters at the beginning of the 
;                   string (or empty string).
;           2- Illegal characters at the end of the string
;           3- Value too large for 32-bit signed integer.

 
strToInt    proc
strToConv   equ     [rbp+16]        ;shadow storage
endPtr      equ     [rbp-8]         ;起local变量endPtr。存strtol结果
            push    rbp             ;存rbp
            mov     rbp, rsp        ;rsp存到rbp
            sub     rsp, 32h        ;开辟空间
            
            mov     strToConv, rcx  ;字符串地址存到strToConv
            
            ;设置strtol的三个参数。rcx为原始字符串没变
            lea     rdx, endPtr     ;获取endPtr地址
            mov     r8d, 10         ;十进制
            call    strtol          ;走strtol
            
; On return:
;
;    RAX-   Contains converted value, if successful.
;    endPtr-Pointer to 1 position beyond last char in string.
;
; If strtol returns with endPtr == strToConv, then there were no
; legal digits at the beginning of the string.

            mov     ecx, 1          ;存结果
            mov     rdx, endPtr
            cmp     rdx, strToConv  ;endPtr存转换结束的第一个字节。如果和strToConv相等说明失败。
            je      returnValue     ;相等的话跳转
            
; If endPtr is not pointing at a zero byte, then we've got
; junk at the end of the string.

            mov     ecx, 2          ;存结果
            mov     rdx, endPtr
            cmp     byte ptr [rdx], 0 ;如果endPtr指向的值不是0,即输入字符串包含其他字符,报错。
            jne     returnValue
            
; If the return result is 7fff_ffffh or 8000_0000h (max long and
; min long, respectively), and the C global _errno variable 
; contains ERANGE, then we've got a range error.

            mov     ecx, 0          ;Assume good input
            cmp     _errno, ERANGE  ;检查c库方面的结果。不是ERANGE就是成功。
            jne     returnValue
            mov     ecx, 3          ;Assume out of range
            cmp     eax, 7fffffffh  ;越界时同时会返回LONG_MAX或LONG_MIN。都是报错
            je      returnValue
            cmp     eax, 80000000h
            je      returnValue
            
; If we get to this point, it's a good number

            mov     ecx, 0 ;到此也是成功
            
returnValue:
            leave  ;返回
            ret
strToInt    endp
            

        
            public  asmMain
asmMain     proc
saveRBX     equ     qword ptr [rbp-8]       ;起local变量
            push    rbp                     ;存rbp
            mov     rbp, rsp                ;存rsp到rbp
            sub     rsp, 48                 ;开辟空间
            
            mov     saveRBX, rbx            ;rbx存到local变量

            
repeatPgm:  lea     rcx, fmtStr1
            call    printf        ;打印
            
            ; Get user input:
            
            lea     rcx, buffer     ;设置参数
            mov     edx, maxLen     ;设置参数
            call    readLine        ;用户输入


            lea     rbx, badInput   ;获取badInput地址
            test    rax, rax        ;函数的返回值在rax。test做and操作,设置zf/sf/pf。
            js      hadError        ;如果sf为1(即为负数)(即rax为负数)(即readLine返回负数),跳转到hadError。
            
            
            lea     rcx, buffer     ;再获取buffer地址。估计rcx为non-volatile,默认不保证其他流程修改过。
            call    strToInt
            lea     rbx, invalid    ;把结果对应的label设置好并跳转
            cmp     ecx, 1
            je      hadError
            cmp     ecx, 2
            je      hadError
            
            lea     rbx, range
            cmp     ecx, 3
            je      hadError
            
            lea     rbx, unknown
            cmp     ecx, 0
            jne     hadError
            
            
; At this point, input is valid and is sitting in EAX.
;
; First, check to see if the user entered 0 (to quit
; the program).

            test    eax, eax        ;返回0结束程序
            je      allDone
            
; However, we need to verify that the number is in the
; range 1-10. 

            ;判断是否在1-10范围
            lea     rbx, range
            cmp     eax, 1
            jl      hadError
            cmp     eax, 10
            jg      hadError
            
; Pretend a bunch of work happens here dealing with the
; input number.

            ;跳转到成功
            lea     rbx, goodInput
            mov     inputValue, eax

; The different code streams all merge together here to
; execute some common code (we'll pretend that happens,
; for brevity, no such code exists here).

hadError:

; At the end of the common code (which doesn't mess with
; RBX), separate into five different code streams based
; on the pointer value in RBX:

            jmp     rbx ;rbx存的是错误对应的label
            
; Transfer here if readLine returned an error:

badInput:   lea     rcx, badInpStr
            call    printf
            jmp     repeatPgm ;报错并进行下一轮输入
            
; Transfer here if there was a non-digit character:
; in the string:
 
invalid:    lea     rcx, invalidStr
            call    printf
            jmp     repeatPgm

; Transfer here if the input value was out of range:
                    
range:      lea     rcx, rangeStr
            call    printf
            jmp     repeatPgm

; Shouldn't ever get here. Happens if strToInt returns
; a value outside the range 0-3.
            
unknown:    lea     rcx, unknownStr
            call    printf
            jmp     repeatPgm

; Transfer down here on a good user input.
            
goodInput:  lea     rcx, goodStr
            mov     edx, inputValue ;Zero extends!
            call    printf      
            jmp     repeatPgm   ;打印并进行下一轮输入

; Branch here when the user selects "quit program" by
; entering the value zero:

allDone:    mov     rbx, saveRBX ;恢复rbx
            leave
            ret     ;Returns to caller
        
asmMain     endp
            end

7.3 Conditional Jump#

根据flag进行各种j

jump有范围

  1. 2字节模式。1字节opcode,1字节位置。可以跳转到正负127字节的地址。

  2. 6字节模式。2字节opcode,4字节位置。可以跳转到正负2G字节的地址。

7.4 Trampolines#

可以突破跳转到正负2G字节地址的限制。
我没仔细看。

7.5 Conditional Move#

根据flag进行mov

7.6 实现基本的控制结构#

如何用asm实现各种控制逻辑

没啥好说的。只能是多看多写。

7.7 状态机和非直接跳转#

看代码

7.8 循环#

看代码

7.9优化循环#

优化loop的小技巧
主要是减少指令的运行

7.10 其他信息#

<<Write Great Code>>

8 运算进阶#

暂忽略

9 数字转换#

暂忽略

10 表查找#

11 SIMD#

single-instruction multiple-data 一个指令作用在多个数据上,处理数据的速度成倍增长。

x86-64的3种vector指令集

  1. Multimedia Extensions(MMX)

  2. Streaming SIMD Extensions(SSE)

  3. Advanced Vector Extensions(AVX)

MMX已经过时,被SSE取代。不再关注。

之前学的经典常见的指令叫做scalar指令集

SSE/AVX指令集的内容和标量指令集差不多,足够写一本书。
这里我们只能入个门。

11.1 SSE/AVX架构#

SSE/AVX都有一堆变种

SSE/AVX的三代:

  1. SSE架构,64bit cpu提供128bit XMM寄存器,支持整数和浮点类型。

  2. AVX/AVX2架构,256bit YMM寄存器。

  3. AVX512架构,能支持最多32个512bit的ZMM寄存器。

这里主要看AVX2和更早的指令。

11.2 Streaming Data Types#

SSE/AVX编程模型支持scalar/vectors两种基本数据类型。

scalar是一个单精度或双精度浮点值。

vectors是多个浮点或int值。
2到32个值,取决于数据类型是byte/word/dword/qword,单精度/双精度,128/256bit。

SSE的XMM寄存器(XMM0-XMM15)(128bit)可以放一个32bit单精度浮点值(一个scalar),或4个单精度浮点值(一个vector)。

AVX的YMM寄存器(YMM0-YMM15)(256bit)可以放8个单精度浮点数。

对于双精度浮点数,容量减半。

对于byte,word等类型,都可以简单按大小往里面摆。


intel把XMM/YMM/ZMM寄存器里的vector项叫做lane

lane可以是8bit/16bit等等。

11.3 用cpuid来区分指令集#

intel在1978年发布8086,之后每一代cpu,基本都会增加指令。

引入了cpuid指令来区分cpu,避免使用不支持的指令。

https://en.wikipedia.org/wiki/CPUID

eax指定想要的信息类别,cpuid指令把相关的信息放到约定的寄存器。
有茫茫多的信息。
具体看intel的cpu文档的Table 3-8. Information Returned by CPUID Instruction

11.4 Segment对齐#

SSE/AVX需要16/32/64字节对齐,masm的align最多只能16字节对齐。

11.5 SSE/AVX/AVX2的Memory Operand Alignment#

12 位操作#

13 宏和编译时语言#

14 字符串操作#

15 管理复杂项目#

15.8 Microsoft Linker和Library#

制作.lib要用lib.exe,和ml64.exe在同个目录下。
https://learn.microsoft.com/en-us/cpp/build/reference/overview-of-lib?view=msvc-170

16 单体asm程序#

之前基本是用一个c.cpp来做入口,把我们的asm代码link上去。

可以写独立的asm程序,不牵扯c/c++库,程序size变小。
但是更麻烦,得自己折腾win32库函数。

https://learn.microsoft.com/en-us/windows/win32/api/

书上说需要去 https://www.masm32.com 下载MASM32 SDK。
为什么是这个sdk?按说应该装最新的windows sdk即可。
估计是微软不维护asm版本的sdk。

64bit版本
https://masm32.com/board/index.php?topic=10880.0

我是之前在vs里装过windows的sdk。
我的lib在C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\um\x64
程序16-1可以不需要MASM32 SDK跑起来。

  1. 在vs里右击cpp文件,properties,item type选为不参与编译。

  2. 右击项目,properties,linker,advanced,entry point填main。

即可独立运行asm程序。

他这两个个sdk分别是2011和2017年左右的。
包含各种头文件/库/macro/demo,还有编辑器。
应该是当时第三方做的一套工具。
虽然老但现在还是能用的,说明abi是兼容的,只是老库功能肯定比现在少。
核心的接口应该变化较少。

https://abi-laboratory.pro/index.php?view=winapi
https://abi-laboratory.pro/compatibility/Windows_8.1_to_Windows_10_1511_10586.494/x86_64/abi_compat_report.html
找到一个win api的abi兼容工具。
可以看到比如kernel32.dll从win8到win10是99%兼容。

我们用到了kernel32.lib。
GetStdHandle和WriteFile都在kernel32.lib中
https://learn.microsoft.com/en-us/windows/console/getstdhandle
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
见页面下方的Requirements

这样基本就清楚了,我们asm直接includelib,把用到的windows的库link上,extrn声明一下,直接调函数可。

16.1#

; Listing 16-1.asm
;
; A standalone assembly language version of 
; the ubiquitous "Hello, World!" program.

; Link in the windows win32 API:

            includelib kernel32.lib

; Here are the two Windows functions we will need
; to send "Hello, World!" to the standard console device:

            extrn __imp_GetStdHandle:proc
            extrn __imp_WriteFile:proc

            .code
hwStr       byte    "Hello World!"
hwLen       =       $-hwStr

; This is the honest-to-goodness assembly language
; main program:

main        proc
            
; On entry, stack is aligned at 8 mod 16. Setting aside 8
; bytes for "bytesWritten" ensures that calls in main have
; their stack aligned to 16 bytes (8 mod 16 inside function),
; as required by the Windows API (which __imp_GetStdHandle and
; __imp_WriteFile use -- they are written in C/C++)
            
            lea     rbx, hwStr
            sub     rsp, 8          ;16字节对齐
            mov     rdi, rsp        ;当前rsp值存到rdi

; Note: must set aside 32 bytes (20h) for shadow registers for
; parameters (just do this once for all functions). Also, WriteFile
; has a 5th argument (which is NULL) so we must set aside 8 bytes
; to hold that pointer (and initialize it to zero). Finally, stack
; must always be 16-byte aligned, so reserve another 8 bytes of storage
; to ensure this.

                                    ; WriteFile有5个参数
                                    ; 32字节固定Shadow storage给4个参数。另一个参数8字节。再来8的对齐。32+8+8=48=0x30
            sub     rsp, 030h       ; Shadow storage for args (always 20h bytes)
                    
; handle = GetStdHandle( -11 );
; Single argument passed in ECX.
; handle returned in RAX.

            mov     rcx, -11    ; -11指定STD_OUTPUT
            call    qword ptr __imp_GetStdHandle ;Returns handle in RAX
                    
; WriteFile( handle, "Hello World!", 12, &bytesWritten, NULL );
; Zero out (set to NULL) "LPOverlapped" argument:
            
            xor     rcx, rcx          ; rcx清零
            mov     [rsp+4*8], rcx    ; 往上走32字节即到第五个参数的位置,清零。即第五个参数传0。
            
            mov     r9, rdi         ; 第四个参数传一个地址,最终输出字节的数量会写到这个地址。用之前的rdi,也就是初始rsp附近。
            mov     r8d, hwLen      ; 第三个参数,string长度。
            lea     rdx, hwStr      ; 第二个参数,string地址。
            mov     rcx, rax        ; 第一个参数,handle,在GetStdHandle返回的rax中。
            call    qword ptr __imp_WriteFile
            
; Clean up stack and return:

            add     rsp, 38h   ; 一共开了38h的内存,收回。
            ret
main        endp
            end

16.2 头文件#

可以include文件

16.3 ABI相关#

win32 api遵循Windows ABI