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中设置项目
new peoject选c++ console app
默认的cpp代码替换为本书的c.cpp代码
source files里添加item,选cpp文件,后缀改为asm即可。填入你的asm代码。
左边explorer里右击项目名,选build dependencies/build customizations,勾上masm。
右击.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.cpp
和listing1-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。
Register Addressing Mode
直接操作寄存器
mov ax, bx ;
PC-Relative Addressing Mode
最常用易懂。mov al, symbol ;
以rip寄存器为基准,获取某个symbol的值。所以也叫作RIP-relative。Register-Indirect Addressing Mode
Indirect意思是不直接使用寄存器的值,而是把寄存器的值当作地址,去操作这个地址上的数据。 必须为64位寄存器。
mov [reg64], al
Indirect-Plus-Offset Addressing Mode
可对寄存器值加减作为地址
mov [reg64 + constant], source
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
做两件事
把call之后的第一个指令的64bit地址(即返回地址)入栈
调转到函数的地址开始执行
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变量的两种属性:scope
和life 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
prologue
和epilogue
选项可帮助生成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对齐。
两种方法
在你的函数里小心翼翼地维护好rsp
在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,收回空间。
这样搞有两个问题:
每次调函数都得add/sub
之前看到的,对齐操作会把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可做三件事
jump跳转执行
call一个label
获取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有范围
2字节模式。1字节opcode,1字节位置。可以跳转到正负127字节的地址。
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指令集
:
Multimedia Extensions(MMX)
Streaming SIMD Extensions(SSE)
Advanced Vector Extensions(AVX)
MMX已经过时,被SSE取代。不再关注。
之前学的经典常见的指令叫做scalar指令集
SSE/AVX指令集的内容和标量指令集差不多,足够写一本书。
这里我们只能入个门。
11.1 SSE/AVX架构#
SSE/AVX都有一堆变种
SSE/AVX的三代:
SSE架构,64bit cpu提供128bit XMM寄存器,支持整数和浮点类型。
AVX/AVX2架构,256bit YMM寄存器。
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跑起来。
在vs里右击cpp文件,properties,item type选为不参与编译。
右击项目,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