汇编系列文章:
https://www.jianshu.com/nb/29822876
寄存器
通用寄存器:
- EAX~EDX,EBP、ESP、ESI、EDI,EFLAGS、EIP
段寄存器:
- CS、SS、DS、ES、FS、GS
寄存器E开头表示32位,H结尾表示高8位,L结尾表示低8位
64位下,原有的32位E开头的变为R开头就是64位,添加R8-R15(R8对应的32位为R8D,16位R8W,8位R8B)
一些特殊用途寄存器:
- EAX:accumulator;
- ECX:loop counter;
- ESP:stack pointer;
- ESI,EDI:index register;
- EBP:extended frame pointer;
- CS:code;
- DS:data;
- SS:stack;
- ES,FS,GS:additional;
- EIP:instruction pointer;
- EFLAGS:flags
- carry:无符号溢出
- overflow:有符号溢出
- sign:负数
- zero
- auxiliary carry:半进位(考虑第4位)
- parity:奇偶校验(1的个数是偶数)
基本符号
数字
+-
号- 进制后缀:h(若以字母开头则需要加个0)、d、b、o
数字表达式
- 优先级:括号>正负>乘除>MOD>加减
字符&字符串 常数
- 单引号双引号都行,一个ASCII一个byte,字符串不会自动加结束符号
保留字 标记符
- 不区分大小写
- ASCII 1-247,首字母为字母_@?$
伪指令directives
- 简化代码,编译器相关,大小写不敏感
指令instructions
- [Label: ] Mnemonic (Operand) [;comment]
- Label:data(不带冒号) 和 code
- 指令注记符&操作数(常量、常量表达式、内存(data label)和寄存器)
- 常量和常量表达式是立即数
- 注释:单行用分号;多行用COMMENT+自己的符号,最后再用这个符号结尾(注意检查中间的代码有没有这个符号)
- 不带操作数的如stc(set carry flag),带一个操作数的如inc
汇编执行
编译-链接-执行
- listing file可以查看自己的程序如何被编译
数据定义
- 内部数据类型(INT)
- 8bit:BYTE、SBYTE
- 16bit:WORD、SWORD
- 32bit:DWORD、SDWORD
- 64bit:QWORD
- 80bit:TBYTE
- 内部数据类型(REAL)
- 4byte(32bit):REAL4
- 8byte:REAL8
- 10byte:REAL10
- 语句
- [name] directive initializer[,initializer…]
- 如:
value1 BYTE 10
- 声明内存中数据
- 可以用?表示未初始化,可以减小生成的exe文件大小
- 数组直接用逗号分开
- 字符串:
- 直接用BYTE,如
str2 BYTE 'Error: halting program',0
- 一行结尾处写逗号,下一行可以继续,如:其中
1
2menu BYTE "Checking Account",0dh,0ah,0dh,0ah,
"1. Create a new account",0dh,0ah,,0dh,0ah
可以换行,前者表示 carriage return回车(及到行首),后者表示到下一行
- 直接用BYTE,如
- 重复 DUP:
var BYTE 10, 3 DUP(1), 2
其实表示var 10, 1, 1, 1, 2
,也就是先写重复个数,再写要重复什么
- WORD可以储存16位整数,也可以储存两个字符
- 小端序:小端存低位,但是像字符串和数组什么的都是按照低位到高位排列的,比如数组
12h, 34h
是从低位到高位,但是12h
内部是1为高位,如果用小端序存储得到的实际存储顺序是34h, 12h
(高位到低位)
符号常量
- 等号伪指令:32bit整数(表达式或者常数),编译器会处理
- 当前位置的计数器
$
:如获取数组长度,可以1
2list BYTE 10,20,30,40
ListSize = ($ - list)- 需要注意:这样计算得到的是byte个数,如果是WORD数组的长度应该除以2
- EQU伪指令:
- 可能是数或者字符串表达式,不可以重定义,如:
1
2PI EQU <3.1416>
pressKey EQU <"Press any key to continue...",0>
- 可能是数或者字符串表达式,不可以重定义,如:
- TEXTEQU伪指令:
- 可以重定义,是宏定义
64位:
- MASM中,支持64位变成,但是不支持INVOKE、ADDR、.model、.386、.stack等
数据转移
直接内存操作:
- 在
.data
中声明的内存地址的label,在代码中可以直接被解引用(dereferenced),可以在外层加上中括号
MOV: - 先destination再source
- 不能两个内存地址
- CS、EIP、IP不能为destination
- DS、SS、ES等也不能直接被立即数mov
- 注意:64位中,如果mov一个32位内存地址的到64位,高位清除,8或者16位不会;但是如果mov一个数,都会直接清除高位
MOVZX: - 可以扩展高位为0后再赋值
MOVSX: - 可以扩展高位为符号位后赋值
XCHG: - 交换,至少一个寄存器,不能有立即数
Direct-Offset Operands: - data label的内容其实是内存地址,所以加上常数相当于进行偏移
加减法:
- INC、DEC:1个操作数
- ADD、SUB:先destination,destination <- destination +/- source
- NEG
- FLAGS:ZF(zero)、SF(sign)、CF(Carry)、OF(Overflow)
数据相关操作
- OFFSET:考虑从数据段开始的偏移
- PTR:可以转换数据类型,只是一个指针,如WORD PTR myDouble就是取myDouble对应内存区域的一个WORD大小的数据
- TYPE:用来获得数据类型的大小(byte数)
- LENGTHOF:计数元素的个数(相当于数组元素的个数)
- SIZEOF:相当于LENGTHOF乘TYPE
- 注:一个声明包含多行的逗号分隔的,但不包含换行后再次写类型的
- LABEL可以用来对同一个内存地址进行不同的解释,在一个声明
intList BYTE 00h,10h,00h,20h
前加上wordList LABEL WORD
可以让这两个label指向同一个内存地址,但是对数据类型的解读不同。
间接寻址indirect addressing
- 直接把地址赋值给一个寄存器,用[]就可以取寻找这个寄存器对应的地址上的内容,相当于在指针前面用*
- 一定要注意这里用的是寄存器,不是label
- 地址赋值时,可以用OFFSET。但是要注意解引用时使用的类型是否与需要的类型一致(如一个WORD的地址,如果赋给eax会有问题,会将之后的地址上一部分也用到,如
tax WORD 0 1
,因为小端序,如果mov eax, OFFSET tax
并且mov ebx, [eax]
就会得到ebx上为65536)。 - 要注意,对于所有的内存地址,增加1只是加1BYTE,如果它本身是WORD或者更大的单位,需要增加2或者更多,或者直接增加TYPE label名称
- 此时如果要对内存地址上的内容增加,不能直接写
INC [eax]
,而要指明上面的类型。如tax WORD 65535 1
,mov eax, OFFSET tax
,进行一些操作后mov ebx, [eax]
,若上述操作是inc DWORD PTR [eax]
则得到131072,否则inc WORD PTR [eax]
则得到65536。
变址操作数Indexed Operands
对于数组来说,可以用[label+reg]
或者label[reg]
的形式访问label代表的数组中的第若干个值,但是不能不在label+reg外面加括号,也不能把reg换成某个label。
其实本质上,label就是一个内存地址,不能让内存地址相加。
例如:
1 | .386 |
索引比例Index Scaling
为了避免上面索引的过程还要判断加几,可以把加的数改成TYPE label名
指针
可以在声明data时直接用ptrW DWORD arrayW
或者ptrW DWORD OFFSET arrayW
把arrayW的内存地址赋值给ptrW的内存地址
JMP
JMP 代码label
直接跳转到对应的代码标签处继续执行。
LOOP
LOOP 代码label
在循环中,每一次都会将ECX减1,之后如果不为0就跳转到代码label处。
注意:如果在循环里面使用invoke,需要首先保存ecx的值,之后再还原
如:
1 | mov ecx, 5 |
如果双层或者多层循环,需要先把外层循环的ecx保留下来,之后恢复。
64位中,使用RCX进行循环计数。
shift&rotate
- 右移时:逻辑移位Logical Shift符号位为0,算术移位Arithmetic Shift符号位为原来的符号位
- SHL、SAL指令为左移,SHR为逻辑右移(符号位为0),SAR为算术右移(符号位保持),丢掉的那一位放在Carry Flag里面
- ROL为向左循环移位,原来的最高位在被移动到最低位的同时也移动到CF里面;ROR向右,CF存储原来的最低位
- RCL为向左循环,把原来的CF拷到最低位,原来的最高位放入CF中;RCR同理,最低位->CF,CF->最高位
- SHLD(Shift left double),操作数是dest,source,n,相当于用source接在dest后面做左移,移出去的放在CF,不改变source;SHRD类似,低位移出去的放在CF,相当于source接在前面
乘除
- MUL是unsigned,把操作数乘AL/AX/EAX/RAX后放在AX/DX:AX/EDX:EAX/RDX:RAX中,若AH/DX/EDX/RDX中非0,会修改CF
- IMUL是signed int mul,结果中会扩展符号位以保持结果的正负,可能修改OF
- DIV是Unsigned,它的操作数为除数,AX/DX:AX/EDX:EAX为被除数,商放在AL/AX/EAX,余数放在AH/DX/EDX
- IDIV是signed integer division,需要符号位扩展(指令有CBW即al扩展到ax,convert byte to word,CWD即ax扩展到dx:ax,CDQ)。注意负数时余数与被除数正负保持一致(49/-5=0xFFF7(-9)…0x0004,-49/-5=0x0009…0xFFFC(-4),-49/5=0xFFF7…0xFFFC)
扩展加减
- ADC在加的时候还加上CF,SBB在减的时候还减去CF,以实现多位的加减