计算机程序基础教程(03):x86读写数据指令
【mov指令】
mov指令用于读写寄存器和内存地址空间,有两个地址码,分别指定写入地址与读取地址,寻址方式有3种:立即寻址、寄存器寻址、内存寻址。
● 立即寻址
也称为立即数寻址,写入的数据存储在地址码中,示例:mov ax,99,将99写入ax寄存器,其中99直接存储在地址码中。
立即数可以使用多种进制方式指定,如下:
99,默认为10进制
10B,添加B后缀,表示2进制
67Q,添加Q后缀,表示8进制
0CH,添加H后缀,表示16进制,若第一个数字为字母则需要额外添加前缀0
0xC,添加0x前缀,表示16进制,某些编译器不支持此方式
● 寄存器寻址
要操作的数据存储在寄存器中,示例:mov ax,bx,将bx中的数据写入ax。
● 内存寻址
要操作的数据在内存中,地址码指定数据的偏移地址,段地址默认存储在ds寄存器中,可以在内存与寄存器之间读写数据,也可以将一个立即数写入到内存,但是不能在两个内存地址之间读写数据。
★ 直接内存寻址
直接内存寻址使用一个立即数指定偏移地址,要操作的内存地址不能改变。
示例:
mov ax,[0x404020] ;将0x404020地址处的数据写入ax寄存器,使用ds作为段地址寄存器。
在寄存器与内存之间读写时,处理器默认要读写的数据长度与寄存器长度相同,处理器通过寄存器的长度确定要读写多少个内存单元。
将一个立即数写入内存时,需要在地址码中指定数据的长度,处理器通过操作数类型码确定要写入几个内存单元。
指定数据长度关键词如下:
byte,1字节
word,2字节
dword,4字节
fword,6字节
qword,8字节
示例:
mov byte[0x404020],9 ;将9写入地址0x404020处,占1个存储单元
mov word[0x404020],9 ;占用2个存储单元
以上为nasm编译器语法,masm编译器需要额外添加ptr关键词:byte ptr。
★ 间接内存寻址
地址码中指定一个寄存器,使用寄存器中的数据作为偏移地址,寄存器中的数据也称为指针,意为它指向另一个数据,需要读写其他内存地址时,读写数据指令无需修改,只需要修改指针的值即可。
示例:
mov ax,[bx] ;[]符号内指定一个寄存器,读取寄存器中的数据作为偏移地址
mov ax,[rcx] ;在x86-64中,第二个地址码只能使用32位、64位寄存器
偏移地址也可以使用多个数据组合的方式指定:
mov eax, [rbx+2] ;寄存器+立即数
mov eax, [rbx+rsi] ;寄存器+寄存器
mov eax, [rbx+rsi+2] ;寄存器+寄存器+立即数,两个寄存器长度必须相同
mov eax, [rbx*4+2] ;寄存器×立即数+立即数
mov eax, [rbx*4+rcx] ;寄存器×立即数+寄存器
● 字节序
对于长度超过一字节的数据,需要使用多个存储单元存储,不同处理器对数据字节的排序方式不同,x86处理器规定数据的低位存储在低地址中,高位存储在高地址中,比如 12345678H 需要占用4个存储单元,拆分为 12H、34H、56H、78H 四个字节,78H是低位,放在低地址中,12H放在高地址中,这种排序方式称为小端序,反之则称为大端序。
【读写并运算】
lea指令的原意为将一个指针写入一个寄存器,功能与mov类似,但是lea不能使用内存寻址,只能使用寄存器寻址、立即数寻址。
lea rax,[0x4] ;将一个立即数写入rax,数据放在[]符号内,但这并非表示内存寻址
lea rax,[rbx] ;将rbx中的数据写入rax,在x86-64中不能使用32位以下寄存器
lea的上述功能与mov指令重合,这并非lea的全部使用方式,lea支持读取数据的同时对数据进行运算,比如:lea rax,[rbp+4],rbp中的数据+4写入rax,等同于如下指令的组合:
mov rax, rbp
add rax, 4
lea支持以下数据运算方式:
lea rax, [rbx+2]
lea rax, [rbx+rcx]
lea rax, [rbx+rcx+2]
lea rax, [rbx*4+2]
lea rax, [rcx+rbx*4]
【读写并扩展】
● movzx
movzx指令用于将一个无符号数扩展长度并写入指定寄存器,扩展的长度由写入寄存器的长度决定,具体行为是将扩展后的高位全部使用0填充,需要扩展的数据可以使用寄存器寻址、内存寻址,若使用内存寻址则需要指定数据长度。
movzx ax, al ;al扩展为ax,ax高位全部设置为0
movzx eax, al
movzx eax, ax
movzx eax, bx
movzx rax, byte[0x404020]
movzx不能用于将32位寄存器数据扩展为64位长度,比如 movzx rax,eax 这样是错误的,因为写入eax时rax的高位会清0,无需扩展,直接使用即可,但是写入ax时eax的高位不会清0,这是x86-64与x86的一个区别。
● movsx
movsx指令用于读取并扩展一个有符号数的长度,具体行为是将扩展长度数据的符号位写入扩展后高位的每一位,若是正数,则高位全部使用0填充,若是负数,则高位全部使用1填充,其中最高位的1表示符号位,其余高位的1表示负数补码,将扩展高位全部设置为1即可满足两个补数相加产生进位的规则。
movsx ax, al ;ax的高8位为al的符号位,若al为负数,则ax的高8位全部为1
movsx eax, al
movsx eax, bx
movsx rax, ebx
movsx rax, byte[0x404020]
【数据扩展指令】
数据扩展指令用于将ax系列寄存器中的有符号数扩展长度。
★ cbw
将al寄存器中的8位有符号数扩展为16位,使用ax寄存器存储,al存储低位,ah存储高位,具体行为是将al寄存器中的符号位写入ah的每一位,若al为负数则ah存储负数补码的高8位。
★ cwde
将ax中的16位有符号数扩展为32位,使用eax存储。
★ cdqe
将eax中的32位有符号数扩展为64位,使用rax存储。
★ cwd
将ax中的16位有符号数扩展为32位,使用dx+ax存储,dx存储高位,ax存储低位。
★ cdq
将eax中的32位有符号数扩展为64位,使用edx+eax存储,edx存储高位,eax存储低位。
★ cqo
将rax中的64位有符号数扩展为128位,使用rdx+rax存储,rdx存储高位,rax存储低位。
【数据交换】
xchg指令用于将两个地址码中的数据进行交换,可以使用寄存器寻址、内存寻址,但是不能使用两个内存地址,这一点与mov相同。
xchg ax,bx
xchg ax,[0x404020]
【读写栈空间】
使用一段内存空间存储数值数据时,若数据比较零散,可以随意的安排读写位置和顺序,而有些数据需要按固定顺序读写,常用的操作顺序有两种:队列和栈。
队列管理方式,最先存储的数据最先使用,最后存储的数据最后使用,就像排队一样,可以通过如下指令实现:
mov rax,0x404020 ;定义读写指针
mov rbx,[rax] ;通过指针读取队列中的数据
add rax,4 ;指针+x,定位到下一个数据,x为数据长度
循环执行以上指令就可以通过队列的方式管理数据。
栈管理方式,最先存储的数据最后使用,最后存储的数据最先使用,就像弹夹一样,最后压入的子弹最先被击发。
若通过多条指令实现栈管理方式会很复杂,执行效率也不高,为了增加效率,CPU提供了专用的寄存器和指令实现栈功能,SS+SP寄存器为栈空间的地址,这个地址称为栈顶指针,栈操作指令通过此地址进行读写数据操作,栈指令读写数据的长度与CPU位宽相同,在8086处理器中,读写操作2个字节,在x86-64处理器中,读写操作8个字节。
● push 入栈指令
push指令用来将数据写入到栈中,有一个地址码,设置入栈的数据,使用立即数寻址、寄存器寻址、内存寻址,示例:push ax。
push对栈的操作是从高地址向低地址的顺序使用的,SP存储栈中末尾数据的地址(若没有数据则存储栈的起始地址),执行push时,CPU首先将SP减去CPU位宽,之后将数据写入SP指定的地址处。
● pop 出栈指令
pop指令用来读取栈中末尾的数据,有一个地址码,设置保存数据的地址,使用寄存器寻址、内存寻址。
pop对栈的操作是从低地址到高地址的顺序使用的,SP存储栈中末尾数据的地址,执行pop时,CPU首先从SP指定的地址处读取数据,读取完毕之后将SP的值增加CPU位宽,定位到下一个数据。
● 使用mov读写栈空间
程序执行时,操作系统会为其分配一段内存当做栈空间使用,栈空间的地址是连续的,读写速度比碎片化的内存更快,但是通过栈指令操作数据不灵活,只能按固定顺序、固定长度进行读写。
为了更灵活的使用栈空间,我们需要使用mov指令自由的读写栈,将一段内存空间当做栈空间使用是我们自己安排的,CPU并不会限制这段空间只能由push/pop指令操作。
mov rax,[rsp]
mov rbx,[rsp+8]
mov rcx,[rsp+16]
使用SP寄存器指定偏移地址时,CPU默认读取SS寄存器作为段地址。
当栈空间需要大量的同时使用mov和push/pop操作时容易导致混乱,为了避免混乱,使用mov操作栈空间时,一般首先将sp的值写入bp,之后将sp减去一个数值,此时sp和bp将栈空间分割为两部分,push/pop通过sp确定数据地址,mov通过bp确定数据地址,分别操作栈空间的不同范围。
mov rbp,rsp
sub rsp,0x60
mov word[rbp-2],1
mov word[rbp-4],2
push rax
push rbx
使用BP指定偏移地址时,CPU也会默认使用SS作为段地址寄存器。
【读写IO地址空间】
内存之外的存储器通过IO地址空间与CPU相连,IO地址空间使用专用的指令进行读写。
● in - 读
in al,0x60 ;读取0x60地址中的数据写入al,只能使用al、ax、eax接收数据,分别表示读取1字节、2字节、4字节
in eax,dx ;可以使用间接寻址,但是只能使用dx寄存器存储IO空间地址
● out - 写
out 0x60,al
out dx,al