汇编笔记
所用书籍:《Assembly Language Succinctly》作者: Christopher Rose
概念
汇编中段的概念
在高级语言中我们常常将(逻辑)代码和数据混在一起,尽管在汇编中也可以,但我们常常不这么做,而是将程序分段。
段的常见形式:
关键字 | 段名 | 作用 |
---|---|---|
.code | 代码段 | 读、执行 |
.data | 数据段 | 读、写 |
.const | 常量段 | 读 |
.data? | 未初始化数据段 | 读、写 |
在常量段中的数据是只读的,而在未初始化数据段中的会被赋值为0
不用.data?
段也可以在.data
段中给变量赋值?
来达到相同目的
段名可以放置在段关键字后,比如 .code myCodeSeg
在数据段中定义的数据,都会被写死在可执行文件中。比如你在数据段中定义了5M的数据,那么你生成的可执行文件至少5M
汇编中标签的概念
汇编中的标签(Label)是在代码段中定义的IP(指令指针)寄存器可以通过 JMP
指令跳到的地方。很像C语言中的goto语句的那个Label(其实是反过来像)。
除了 JMP
指令外,还可以用 Jcc
(条件跳转)和 CALL
指令。
还可以将标签存在一个寄存器里,然后间接地实现跳转。那这个寄存器其实起了一个指针的作用。
例子:
SomeLabel:
mov rax, SomeLabel
jmp rax
匿名标签
用 @@:
这样的方式来定义,然后调用的时候通过 JMP @F
来跳转到下一个匿名标签,相反用 JMP @B
来跳转到上一个。
匿名标签虽好,可不要贪杯哦!~
汇编中的数据类型
汇编有高级语言有的基本数据类型,但是名字不太一样。
数据(类型)占的存储空间的大小在汇编中十分重要。在C语言中,int到底是4个字节还是8个字节,其实我们不是那么关心的。可是在汇编中不行,因为它不能自动帮我们完成指针的算术运算。在C语言中,当给int*
类型的指针做算术运算的时候,是会自动乘以int的长度的。
比如:
int* p;
p = p + 1;
那这个时候p被加上的是int的长度(一般是4)而不是1。
这一切在C语言中都是自动完成的,而在汇编中需要手动完成。所以说,数据类型的大小长度,在汇编中十分重要。
数据类型对照表
类型 | 汇编 | C++ | 位(bit) | 字节(byte) |
---|---|---|---|---|
Byte | byte(db) | char | 8 | 1 |
Signed byte | sbyte | char | 8 | 1 |
Word | word(dw) | unsigned short | 16 | 2 |
Signed word | sword | short | 16 | 2 |
Double word | dword | unsigned int | 32 | 4 |
Signed double word | sdword | int | 32 | 4 |
Quad word | qword(dq) | unsigned long long | 64 | 8 |
Signed quad word | sqword | long long | 64 | 8 |
XMM word(dqword) | xmmword | 128 | 16 | |
YMM word | ymmword | 128 | 16 | |
Single | real4 | float | 32 | 4 |
Double | real8 | double | 64 | 8 |
Ten byte float | real10(tbyte,dt) | 80 | 10 |
关于存储空间
依速度快慢排序分别是(越靠近CPU越快):
硬盘或外部存储设备(巨大往往大于100G) < 内存(大,GB级别) < CPU缓存(小MB级别) < CPU寄存器(极小,小于1KB)
在汇编中最重要的两个存储是CPU和内存
在32位时代,内存是分段(segment)的,如今我们用的是平面(flat)内存模型,这种内存可以看成是一个巨长的1维数组。
从内存地址中取得的数据,往往已经被存到了CPU的L1缓存中了。
CPU的寄存器
寄存器就是CPU上的变量(variables),它们没有数据类型,或者说,它们有所有的数据类型(因为在汇编中,数据类型在存储上,就是长度不同的空间罢了)。
然后它们也没有地址,因为它们不在内存里边,根本不需要。所以我们不可以用指针啦、饮用啦来访问它们。就像前边说的,它们本身就是是变量。
然后呢,就是CPU的寄存器设计必须向旧版本兼容,因为如果不这样的话,那么针对原来版本CPU的程序就都GG了。。。这也引出了一个好玩的概念:CPU为什么有32位、64位之分。
20世纪70年代(1970s),哈哈又是1970!那时候的著名8086CPU(为什么我们现在用一个词叫x86,就是由此来的)是16位的。
那问题来了,为啥叫16位来?因为此CPU一个寄存器的大小占16个bit,也就是两个字节。CPU的字的概念就这么来了,16位CPU的字长就是两字节。
8086CPU的寄存器大概如下:
寄存器名 | 解释 | 是否可分 |
---|---|---|
AX(AH,AL) | Accumulator | 可分为两个8位寄存器,AH,AL |
BX(BH,BL) | Base | 可分为两个8位寄存器,BH,BL |
CX(CH,CL) | Counter | 可分为两个8位寄存器,CH,CL |
DX(DH,DL) | Data | 可分为两个8位寄存器,DH,DL |
SI | Source Index | 不可分割 |
DI | Destination Index | 不可分割 |
BP | Base Pointer | 不可分割 |
SP | Stack Pointer | 不可分割 |
IP | Instruction Pointer | 不可分割 |
Flags | Flags | 不可分割 |
SS | Stack Segment | 不可分割 |
CS | Code Segment | 不可分割 |
DS | Data Segment | 不可分割 |
ES | Extra Segment | 不可分割 |
地址模式
地址模式指的是,汇编指令所接受的参数的类型的差异。
分别有如下几种:
寄存器地址模式
mov eax,ebx
就是原地址和目标地址都是寄存器的直接地址模式
mov eax,128
就是直接往寄存器里存数字 往寄存器里存表达式也属于这种,因为所有的可求出定制的表达式都会被先求值再放进寄存器的隐式地址模式 就是有些指令中不出现寄存器的名字,但却用到了那些寄存器
内存地址模式 就是在数据段定义变量,然后在代码段使用。
比如
.data
myVar db ?
.code
mov al, myVar
然后这些变量其实是指向内存地址的指针,那条语句也可以这么写 mov al, byte ptr [myVar]
- SIB地址模式
然后寄存器也可以放到指针的方括号里(可以放进一个和两个去,不知道能不能放3个),然后通过类似C语言中数组下标计算公式的方式来在内存中穿梭。
但有一点值得注意的是,汇编并不会像C语言那样,自动的做指针的算术运算,所以这一点我们得自己完成。
所以,这一地址模式(S是scale,I是index,B是base)将表现为如下形式:
mov bx, byte ptr[rcx+rdx*2]
add qword prt[rax+rcx*8], r12
数据类型 ptr[base+index*scale]
这个base位置的寄存器里存的就是你要访问的起始位置的指针,就像C语言里数组的首元素地址。然后这个scale就是我们上边说的,需要我们自己手动设置的指针类型,也就是这个类型的变量要占多少个字节。然后,这里的index,就是我们在C语言中写的数组的下标。
所以,同样的东西在C语言里会这么写:
*(base+index);
相当于
base[index]
数据段与变量定义
在数据段中定义的变量都会被写入目标可执行文件中(说白了就是会占空间)。
普通变量
定义普通变量,实际上是定义了一个相对于数据段起始位置的偏移量的别名(变量名)。而且,这些变量名都是指针。
变量名除了C语言的规则外,还可以用@和?
语法如下:
变量名 类型 (可选的)初始值
a dw 0
b db ?
c dt 0.0
虽说初始值是可选的,可是还是得显式写个 ?
,空着是不行的。
数组
数组定义有两种方式,一种是普通的定义,另一种是快速定义。
数组是相同类型的一组变量,存在连续的空间里。而在汇编中,数组就是一块内存空间,且首元素被给了个名。
普通的定义:
数组名 类型名 数据(用逗号隔开)
myArr1 dw 1,2,3,4
myArr2 dw 5,6,
7,8
第二个数组的意思是,要换行的话,就在第一行尾留一个逗号,下一行接着写就行了。
快速定义:
快速定义就是用一个特殊的语法,让特定的序列重复(duplicate)特定的次数。这也就意味着,要手动指定一个次数(注意,在序列长度大于一的时候,这个次数并不代表数组长度)。
语法如下:
数组名 类型 次数 dup(序列)
myArr1 byte 3 dup(1,2)
这个数组等价于
myArr2 byte 1,2,1,2,1,2
所以,用这种方式定义的数组,长度是重复次数乘以序列的长度。
还有就是dup这个语法可以嵌套。
获取数组的信息:
sizeof myArr
返回数组的字节数
lengthof myArr
返回数组的元素个数
type myArr
返回数组元素的类型(即所占字节数)
字符串
在C语言中的字符串都是零分字符串,即结尾都有一个’\0’。为了适应,我们在汇编中要手动加上这个0
语法如下:
myStr1 db "hello world!",0
myStr2 db "hello ",
"world!",
0
通过以上示例可看出,字符串和数组表现出极其相似的性质。而(我猜测)实际上双引号的作用就是把这些字符串转换成它们的ASCII码,然后变成数组的形式。
Typedef
用 typedef
关键字可以为现有类型定义别名(只要不与预留字冲突即可),语法如下:
别名 typedef 原类型
integer typedef sword
结构体
结构体定义语法如下:
结构体名 struct
变量名 类型 初始值
结构提名 ends
myStruct struc
X word 0
Y word 1
myStruct ends
不是我写错了,就是也可以写成struc
创建一个结构体实例的语法如下:
p1 myStruct {} ;用初始值去实例化一个结构体
p2 myStruct {10,?} ;用指定值去实例化一个结构体
p3 myStruct {5,} ;只填写部分值,而此时p3的Y的初始值不是1,而是0
所以需要注意的是,用初始值去实例化一个结构体时,初始值是从第一个变量的初始值依次来选用的,而不是对应当前变量位置的初始值。
要取得或者改变这些成员变量的值时,可以像C语言中那样用 .
连接它们。如, p1.X
而在需要用指向它们的指针,以在函数中改变它们本体的值的时候,可以用 LEA
这个指令。
语法如下:
lea rcx, p1
mov [rcx].myStruct.X, 200
用取到的地址的语法比较奇怪,需要先用中括号把得到的指针括起来,后边再点上它的结构体类型才算完成指针运算。
另外,结构体可以嵌套定义。
共用体
与C语言的基本类似,不同的地方与结构体类似。
把命令存在常量中
现在一些比较新的语言都可以解析变量中的语句,予以执行(对,多出现于解释执行的语言)。
而这一功能汇编也有。语法如下:
常量名 equ <汇编指令>
storeDrct equ <mov eax,23>
;;在调用的之后,就会执行尖括号中的指令
NoOp equ <db 90h>
;;代码段中直接出现db然后跟数字会被解析成机器码执行
win32汇编
命令:
mov ax,X 将X移动进ax中,即赋值操作
add ax,X 将X加到ax上,即+=操作
sub ax,X 同上,-=
标记:
loop 循环至标记处
assume cs:X 指定代码为什么段
code segment 代码段开始
- code end 代码段结束
概念:
- 安全内存区域 位于0:200-0:2FF的内存区域是安全的