本节内容
- 寄存器
- 几类寄存器
- 内存寻址
- 常用汇编指令
- 操作数长度
- MS-DOS debug.com[付费阅读]
- 简读汇编代码[付费阅读]
寄存器
学习汇编之前,首先需要了解寄存器。寄存器其实很简单,你把它想象成几个特殊的内存块就可以了。
整个存储体系是金字塔结构,最上面我们就称之为寄存器,离内核最近,光处理速度最快,震荡周期最短。
寄存器看成几个很特殊的内存块,这几个寄存器因为很特殊,它并没有地址这样的概念,简单的做法就是起个名字,例如AX、BX、BP、SP。寄存器在编译器使用当中有些特殊的用途,有通用的什么都能干、还有写专门用途的比如BP、SP用来维持堆栈的。当然每种编译器怎么使用由编译器定义,比如C语言编译器BP描述栈底的位置,其他编译器BP表示通用寄存器。也就是说寄存器虽然有些特殊的规则,但是这些规则是由编译器决定的,编译器愿意遵守这个规则就是这个规则不愿意遵守就不是这个规则。除此之外还有些规则是由CPU决定的,因为CPU有自己的规则,比如IP寄存器。CPU需要对它做专门的处理。
寄存器在不同的CPU中数量不一致的。有些通用的寄存器所有CPU都有的,相当于一个规范。所以当编译器想让代码具备通用性的时候,那么它就会尽可能使用通用寄存器。有些时候编译可以专门针对某种CPU进行优化,可以使用一些专用寄存器,例如英特尔的编译器专门针对英特尔CPU进行优化。也可以针对某种场景做优化,例如gcc专用的编译开关用于控制生成某种特定架构的代码这样来提升速度。我们通常情况下不会关注这些细节,因为这都是编译器要处理的事情。我们自己不去写汇编代码,那么我们阅读这些汇编代码只需要每行汇编代码究竟什么意思就可以了。
Native Code其实就是一些二进制的数据,这些数据当作指令(code)和数据(data)。CPU只能处理二进制数据,实际上包括十进制、十六进制、八进制对于CPU来说这种东西不存在的,这些仅是这些数据用其他符号进行表达。比如某段二进制数据的code表达为Inc,data表达为AX。这样两个符号Inc AX就代表某段二进制数据,方便于记忆。编译器会把这些符号翻译为二进制数据,这样我们只需要在编译上维持这种映射关系就可以了,不需要人工去维护这种东西,这就是汇编的由来。汇编并不等于Native Code,这是两码事。
汇编语言是世界上最简单的语言,汇编世界里面所有事情都被简化没有任何抽象,只是对数据的处理。那么把数据从一个地址移到另外一个地址,要么对数据进行加法减法操作,或者比较两个地址中数据大小。汇编的时间里全是数据,这些数据全部都是数字。所以汇编才是这个世界上最简单的语言。汇编没有复杂度,需要的是耐心。
几类寄存器
我们把汇编里所需要的寄存器分下类,比如
- 通用寄存器:AX,BX,CX,DX,SI,DI,(BP,SP)
一共有八个,但是BP、SP有另外的用途,通常情况下我们不会直接去用它,自己用也可以,需要去维护一些特殊的状态。
- 堆栈寄存器:BP(基址),SP(栈顶)
维持函数调用堆栈的。
- 指令指针寄存器:IP,PC(程序计数器,地址)=CS:IP
IP是告诉CPU下一条处理的指令在哪里,我们知道汇编指令一行一行的,每行都有地址,那么比如执行到某一行,比较指令的话可能会跳到另外的地方,必须要有个东西记录这个信息告诉下一条指令在哪,这样CPU才知道执行哪个,因为CPU本身并不维持这种上下文,得有个东西记录这个状态,IP寄存器就是干这种事。
我们经常看到PC(程序计数器,地址),在很多文档上IP寄存器又称之为PC,这是有个说法的,PC寄存器严格来说不是个寄存器,它代表的是一个地址,下条执行指令的地址在哪,它完整表达意义是CS:IP共同组成的,在64位下IP和PC的值通常是一样的。
- 段寄存器:DS(数据data),CS(代码text) (8086 16位CPU,20位地址总线)
在早期8086状态下的时候它有这样一个问题。我们知道CPU进行操作时候,需要总线传输这些指令,总线包含地址总线、数据总线、控制总线。在读取数据时,首先把地址通过地址总线发送,控制总线控制读写命令,数据通过数据总线返回。但是在8086情况下,地址总线的宽度是20bit,最大寻址能力是1<<20即1MB。问题出在8086本身CPU是16bit,也就是说寄存器可以存的最大数据是1<<16即64KB,也就是说我们在寄存器里面最大保存的寻址范围64KB。那么我们怎么样去查64KB以上的数据呢?比如说我传一个数据的时候首先需要把地址存到寄存器里面,问题是寄存器最大只能保存0-64KB的地址,那么后面那块地址怎么办,显然寄存器是存不下的,那么想个什么方法呢?可以把地址拆分为两块,第一块表示某一段,就像我们数据不够用的时候,比如门牌号,先化成区,A小区的6号,B小区的6号,A小区加上6和B小区加上6实际上就形成了分段加偏移量这种格局,所以这样一来它把这个地址分段,当然这个段实际上不是固定的。用段加偏移量这种方式构建完整的字符串,这样好处我们可以用两个寄存器保存数据,比如说某个数据我用DS保存某一段,然后用AX表示偏移量。这样一来两个16位寄存器就可以寻址64KB-1MB的空间。这是因为早期这种寄存器的宽度不够。
寄存器有不同的宽度,RAX,EAX,AX,AH/AL,R开头的表示64位,E开头的表示32位,默认16位,16位可以分为两个8位寄存器。我们写汇编代码时,不需要指定多少位,直接写AX,编译器会根据你编译的架构是什么样的,它会把它翻译时候会加上前缀。
内存寻址
在内存寻址,有些寄存器有很大的区别,像我们在普通内存操作时候。
直接操作:我们写源码时候声明内存时候给它一个符号x,这个符号就代表这段内存,如果x=100,就相当于在某段内存中写入100,x不是指针而直接表示这段内存直接对内存操作。
指针间接操作:我们可以创建一个指针,指针也是标准的变量,指针里面保存了指向x的地址,然后我们可以通过指针间接的去赋值操作。*p=100。
我们普通的内存操作就是这两种方式。我们对寄存器操作的时候它有一些固定的语法:
- 直接将操作数存入寄存器:mov AX,5
- 从指定地址读取数据,存入寄存器:mov AX,[1000] 完整操作1000 => DS:1000,类似 AX=*p
- 在不同寄存器间复制数据:mov AX,BX
- 从寄存器保存的内存地址读取数据:mov AX,[BX]
- 指定地址偏移量,读取内存数据:mov AX,[BX+SI];mov AX [BX+4] [BX+4]不同写法4[BX],4(BX)
常用汇编指令
- 数据传递(mov)
- 数据加减(add,sub)
- 跳转指令(jmp)
- 指针传递(lea):lea AX [1000] 直接把1000地址写到AX中,不取DS:1000指向的data。类似 AX=p
- 栈操作(pop,push)
- 函数调用(call),当前函数执行结束(ret)
操作数长度
- byte:8位
- word:16位
- dword:32位。mov DWORD PTR,movl
- qword:64位。mov QWORD PTR,movq
MS-DOS debug.com[付费阅读]
简读汇编代码[付费阅读]
这个系列的每篇文章有大半篇幅内容属于付费阅读。提供微信支付或支付宝支付打赏50元备注留言手动提供付费文章访问密码。