汇编指令

汇编系列文章:
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
      2
      menu BYTE "Checking Account",0dh,0ah,0dh,0ah,
      "1. Create a new account",0dh,0ah,
      其中,0dh,0ah可以换行,前者表示 carriage return回车(及到行首),后者表示到下一行
  • 重复 DUP:
    • var BYTE 10, 3 DUP(1), 2 其实表示var 10, 1, 1, 1, 2,也就是先写重复个数,再写要重复什么
  • WORD可以储存16位整数,也可以储存两个字符
  • 小端序:小端存低位,但是像字符串和数组什么的都是按照低位到高位排列的,比如数组12h, 34h是从低位到高位,但是12h内部是1为高位,如果用小端序存储得到的实际存储顺序是34h, 12h(高位到低位)

符号常量

  • 等号伪指令:32bit整数(表达式或者常数),编译器会处理
  • 当前位置的计数器$:如获取数组长度,可以
    1
    2
    list BYTE 10,20,30,40
    ListSize = ($ - list)
    • 需要注意:这样计算得到的是byte个数,如果是WORD数组的长度应该除以2
  • EQU伪指令:
    • 可能是数或者字符串表达式,不可以重定义,如:
      1
      2
      PI 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 1mov 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.386
.model flat, stdcall

include msvcrt.inc
includelib msvcrt.lib

.data
szFmt db 'Now: %d', 0dh, 0ah, 0
tax DWORD 1, 2

.code
start:
mov ebx, tax
invoke crt_printf, addr szFmt, ebx ;1
mov ebx, [tax]
invoke crt_printf, addr szFmt, ebx ;1
mov ebx, tax + 4
invoke crt_printf, addr szFmt, ebx ;2 因为DWORD所以加4
mov ebx, [tax + 4]
invoke crt_printf, addr szFmt, ebx ;2
mov ebx, tax[4]
invoke crt_printf, addr szFmt, ebx ;2
mov eax, 4
;mov ebx, tax + eax
;invoke crt_printf, addr szFmt, ebx ; 报错
mov ebx, [tax + eax]
invoke crt_printf, addr szFmt, ebx ;2
mov eax, 4 ; invoke这个函数时会改掉eax,所以需要再次mov
mov ebx, tax[eax]
invoke crt_printf, addr szFmt, ebx ;2
ret
end start

索引比例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
2
3
4
5
6
7
	mov ecx, 5
l1:
mov eax, ecx
pushad
invoke crt_printf, addr szFmt, eax
popad
loop l1

如果双层或者多层循环,需要先把外层循环的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,以实现多位的加减