x86汇编(转)
本文转自知乎《x86汇编语言》,主要是为了后续方便自己学习,在此做个备份,以免文章丢失。
1. 指令的概念
1.1 寄存器和加法机
1) 用电表示数字
用导线传递信号,有电 (高电平) 的一根线代表传递的信号是1
,没电 (低电平) 的一根代表传递的信号是0
。这样就能用信号线来表示一个二进制数字01000100
B,如下图,该二进制数转换成10进制就是68。
2) 二进制加法器
加法电路实现了传入两个数字,然后输出两个数字相加的结果的功能。如下图,两个数字是通过两组,每组 8 根信号线来输入的,输出是通过 8 根信号线输出的。
通过左边和下面的开关的断开与闭合,可以让信号线输入高电平和低电平,最终结果通过灯泡显示出来。
3) 寄存器
现在电路的状态没有被保存下来,断开开关 灯泡就会灭,接上开关 灯泡就会亮。现在在开关和灯泡之间增加一个触发器,如下图,触发器的左边是输入,右边是输出。触发器可以把输入信号保存起来,同时可以在输出信号端看到锁存的内容。触发器什么时候锁存是可以控制的,由下面一个按键开关来决定,按下按键开关就会将输入锁存起来。
上图按下开关后,触发器就会一直输出锁存的信号 1。后续输入再怎么变化,都不会影响锁存的内容,除非再次按下按键开关发送锁存命令,触发器锁存新的输入信号。
一个触发器只能保存 1bit 的数据,为了保存一个完整的二进制数,可以使用若干触发器,将它们组合在一起,形成一个新的器件,叫做寄存器。下图的寄存器有 8 个输入,当一个锁存命令进来的时候,寄存器可以同时保存 8bit 的数据。
4) 带寄存器的四则运算机器
在新的加法电路里面增加寄存器 R。现在不需要两组输入信号,只需要一组输入信号。因为输入第一个数字的信号后,可以按预置按钮把第一个数字的信号保存在寄存器 R 中,然后输入信号线上输入第二个数字的信号,按相加的按钮把锁存在寄存器 R 里面的数字和信号线上输入的数字相加,得到右边输出信号线上的数字。
对上面的加法机进行改进增加四则运算,它就变成了四则运算机。
1.2 机器指令
现在可以通过指令来控制运算器以完成运算过程。上面的四则运算机只有一个寄存器,在进行复杂的运算的时候肯定是不够用的,要存储很很多中间值。例如:(207+9)÷(56-48)
,要先做加法和减法,得到的两个结果再做除法,没有新的寄存器我们就需要手工记下来,这里暂时增加一个寄存器Z
增加新的寄存器,随之而来的增加了很多很多的命令。如果还用原来的一个控制线控制一条命令,就需要增加很多控制线来执行这些命令,所以需要做出改变。既然能用一排开关组合出需要运算的数字,我们也可以用一排开关组合出要执行的指令。右边的表格是指令的二进制数和它对应的操作。
计算(207+9)÷(56-48)
:
1) 第1步 左边输入设置成 207,指令设置成 0_0001B,然后执行。207 被存入寄存器 R;
2) 第2步 左边输入设置成 9,指令设置成 0_0101B,然后执行。9 和 207 相加,结果存入寄存器 R;
3) 第3步 左边输入设置成 56,指令设置成 0_0010B,然后执行。56 被存入寄存器 Z;
4) 第4步 左边输入设置成 48,指令设置成 0_1010B,然后执行。56 和 48 相减,结果存入寄存器 Z;
5) 第5步 指令设置成 1_0100,然后执行。寄存器 Z 和 R 相除,结果存入寄存器 Z。
2. 内存和自动计算
2.1 内存
上面机器指令章节中,改进方法是可以把代表指令的二进制数保存到内存里面,让机器一条一条取出来执行,这样就不用每次手动拨动开关来指定指令了。
内存是由大量的内存单元堆叠而成,下图每一个方块都是一个内存单元。在主流的计算机中,每个内存单元的长度是 8bit。每个内存单元都有唯一编号,从编号 0 开始,这些编号也被称为地址。内存旁边有一排地址线,来用来选中要读写内存单元的编号。下图有 8 根地址线,这 8 根地址线可以访问 0x00-0xFF 内存地址的内存,地址线越多,能访问的内存空间越大。
除了地址线以外,还有数据线和控制线,可以写入和写出内存数据。
2.2 自动计算
有了内存之后,我们需要建造一台可以自动取指令并执行指令的运算机器。为了跟踪每一条需要执行的指令,运算器内部有一个指令指针寄存器,这个寄存器保存着要执行指令的地址。
刚开始的时候,这个寄存器保存着第一条要执行的指令的地址。当运算器开始工作的时候,把指令指针寄存器的内容放到放到地址线上,然后内存把该地址线指向的内存地址里面的内容放到数据线上,内存中的数据通过数据线进入运算器。因为现在是取指令阶段,所以运算器在收到数据后把它当成是指令而不是普通数据,然后运算器根据指令的内容做相应的操作,与此同时 指令指针寄存器的内容被自动修改位下一条指令的地址。
我们来看下面这个例子,首先指令指针寄存器指向 0x00 地址处,该地址处的指令的功能是将 0x01 地址处的值传送到寄存器 R 中。当执行完 0x00 地址处的指令,指令指针寄存器指向 0x02 处。每一条指令的长度可能是 1 或者 2,处理器会根据执行的指令判断出下一条指令的位置,从而更新指令指针寄存器中的内容。最终运算的结果存储在内存 0x0C 处。
3. Intel x86 处理器的发展
8086 处理器->80286 处理器->80386 处理器->80486 处理器->奔腾处理器。
4. 8086的寄存器
4.1 通用寄存器
通用寄存器就是用来存储临时变量的,和上面介绍的寄存器 R、Z 类似。INTEL 8086 处理器有 8 个通用寄存器,每个寄存器是 16 位。8 个通用寄存器分别是 AX、BX、CX、DX、SI、DI、SP、BP。
AX 寄存器的高 8 位是 AH 寄存器,低 8 位是 AL 寄存器,BX、CX、DX 和 AX 寄存器类似。
4.2 程序的分段
下图,左边是内存中我们编写的指令和数据,右边 IPR 寄存器指向的是正在执行的代码在内存中的位置,也就是现在在执行内存地址 0x1000 处的指令。当 0x1000 处的指令执行完了以后,IPR 中的地址修改为 0x1003,执行 0x1003 处的指令。以此类推,在每次执行完一条语句之后 IPR 指向位置就会改变
8086 处理器的地址线是 20 根,地址是 0-0xFFFFF。IPR 是 16 位寄存器,只能访问 0-0xFFFF 地址,无法访问全部的 0-0xFFFFF 内存。
程序分段的方式访问内存*
我们使用 CS 和 IP 来完成 IPR 寄存器的功能,就可以访问 0-0xFFFFF 的内存了。CS 和 IP 寄存器都是 16 位,$CS * 16 + IP$ 的值指向了当前当前正在执行的指令,当这条语句执行完成之后,IP 寄存器指向下一条指令,CS 寄存器的值不改变。如下图,$CS * 16$ 相当于 0x30CE 变成了 0x30CE0。IP 一开始的值是 0,此时$CS * 16 + IP = 0x30CE0$;执行完第一条指令之后,IP = 0x03,此时 $CS * 16 + IP = 0x30CE3$;以此类推。
总结一下,CS 寄存器中存储的是段地址可以任意选取,配合 IP 就可以访问 8086 可以访问的所有内存地址。
4.3 8086所有寄存器
5. 汇编语言
5.1 为什么要用汇编
因为机器码比较难记忆,所以使用汇编这样的简单字母组合来替代机器码
5.2 汇编语言编写
1) 在文本编辑器中可以编写汇编程序
2) 汇编程序不区分大小写
3) H 表示 16 进制,可以改成 0x… 或者直接写十进制数字,或者改写成二进制 00001111B
4) mov 是把右边的数字保存在左边的寄存器中
add 是把右边的数字和左边的数字相加,结果保存在左边的寄存器中
5) 汇编程序文件的后缀为 asm,保存 asm
6) 汇编中的注释使用;
5.3 汇编语言编译
1) 编译程序使用 nasm
2) 把这个目录配置到环境变量中
3) 编译源文件
-
-f
指定输出格式 -
-o
指定输出名字
下面生成了 CPU 指令存储在 .bin
文件中:
bin 格式只包含处理器指令,别的什么也没有,windows 要求除了处理器指令,还要在处理器指令的前面包含一些信息来介绍这个处理器指令怎么使用,以此形成 .exe 文件。这个我们后面再介绍,现在仅可以使用十六进制查看汇编编译后的源码
4) 16进制查看源文件和 bin 文件
上图B8
是机器指令,我们可以翻阅 intel 手册查看其功能,这个在后面会说。
[参看]