汇编打怪升级(1):基础知识

993 阅读20分钟

计算机程序与汇编语言

如我们所了解的,程序的本质是输入/输出的过程,包罗万象又绝非偶然。正如我们生活中无时无刻都在输入/输出。眼观、耳闻、鼻嗅、指触等都是接受输入信息的方式,然后经过大脑或身体本能的处理,产生输出——最后做出各式行动。只不过这些过程已自然而然、浑然一体,因此不易察觉。

计算机程序,自然地符合人们对世界做出响应的方式,接收一段信息的输入,经过特定的处理,产生输出,更进一步地,在特定的环境下,输出将会有不同的解释。如同电影桥段“掷杯为号”,这样的输入,可能最终输出是发出了脆响,杯子也碎了,然后“涌现五百刀斧手”,或是“跳出个人:‘对不起,我是卧底’”。

使用数学语言描述,是 y=f(x),x是输入,f()是处理,y是输出。y=f(x) 既可以抽象地描述我们观察世界的方式,也可以描述计算机中最核心的部件——CPU的核心功能——计算。

CPU工作本质.png

对于CPU而言,它的工作与内存密不可分,大可以说,CPU只认识内存。内存中即存放了f()的处理代码,又存放了输入数据X,还存放了经过f(x)处理都的数据y。

观察这个过程,可以发现:

  • 需要有访问某段内存地址的方法,这样可以找到f()、x
  • 需要有方式能读出f()、x包含的信息
  • 需要有执行各类运算的方式,这样就可以执行f()
  • 需要有方式能存入y

无论程序多复杂,实际运行的过程不外如是。这些需要都可以由机器码来编写机器指令来完成,但是,仅有“0”和“1”构成的机器码,无论是阅读上还是书写上,对人类太不友好。于此,就有了汇编语言,以符合人类阅读与书写的方式,来协助完成对底层执行的逻辑控制。 最后,汇编语言 + 编译器 -> 机器码 = 所编写的程序的具体处理过程。

现在,我们以8086CPU为操作环境,学习要窥探汇编语言思想,所要具备的基础知识。如无特殊说明,均指环境为8086。

寄存器

考虑最简单的运算 c = a + b ,a与b为参与运算的因子,c则作为结果。虽然 a与b 可以来自于内存,但参与计算本身的信息的存储位置,应于它的来源位置无关,因此,需要额外的地方来存储这一部分信息。

为了保证运算本身能完好执行,而需要的存储信息的存储部件,就是寄存器。现在 a 可以放在寄存器1 而 b可以放在寄存器2 中。再如,a与b的来源的寻找依据,c的去处的依据,这些信息同样都可以在寄存器中。

在8086中有14个寄存器来帮助完成运算工作,每个寄存器都有各自的名字,以不同的功用可以分为段寄存器专用寄存器通用寄存器指针寄存器,待到使用时,进一步说明。

每个寄存器为16位,即可以表示16个二进制的信息。为了方便阅读,信息内容以16进制来表示,如 "0010 0101 0110 1100 B" = “256C H”,“B”表示这是一个二进制数,“H”表示这是一个16进制数。

总线

是否有想过,为什么计算机内的信息要以二进制来表示?

答案与传播信息的电子元件有关。一根导线,可以以某一极短时间内的高低电平来分别表示“0”和“1”。当电伏达到某个阈值时为高电平即1,某则为低电平即0。大可以很不严谨地理解为,通电了就是1,没有通电就是0。因此一根导线在某个极短的时间段内只能代表两种状态中的一个。那么观察一段时间,我们可以得到的信息可能就是“1 0 0 1”。

当然,为了效率,可以一次是使用多跟导线进行观察

多条导线传输的数据.png

像上面,一次就可以得到信息 “1001 1101” 。

这些信息本身是无意义的,而当我们制定了规则之后,不同的信息值就有了含义。如同有时我们制定暗号,1代表打游戏,2代表没空,3代表出门等等。

那么,对于将所有存储部件都当做是内存的CPU来说,就通过总线来与CPU交换信息

CPU中的总线.png

其中:

  • 地址总线:可以对存储单元寻址。
  • 数据总线:可以向目标存储单元读写数据。
  • 控制总线:决定了对CPU外部器件的控制能力。

如果有10根地址总线,就说地址总线的宽度为10,那么它一次传输的数据,就有 2^10 = 1024 种可能性。

存储器

对于拥有存储能力的存储部件,都可以将它视为存储器。在存储器中,会将存储区域,等量地划分为若干个存储单元,每个存储单元可以存储8个二进制信息,也是1Byte,即字节。

存储器.png

每一个存储单元,都拥有一个逻辑编号,这样,就可以根据编号寻找到目的存储单元。

当然,存储容量可以有多种等价的表示方式,每种单位间的换算为

1TB = 1024GB
1GB = 1024MB
1MB = 1024KB
1KB = 1024B
1B = 8Bit

现在,观察CPU与内存间交换信息,如读取一个数据的过程就可以表现为:

从存储器上读写.png

  1. CPU通过地址总线发所能信息,寻找位于X地址上的存储单元
  2. CPU通过控制总线,向内存发送读写命令
  3. 内存通过数据总线,将X地址上的数据 Y 发送给CPU

总观来看,地址总线找到信息的位置,数据拿到位置上的数据,而控制总线决定找到哪个存储部件。实际上,在CPU的视角里,所有与它通过总线相关联的存储设备都被视为内存,并从内存的某个地方拿到数据输入,进行运算,最后输出到内存的某个地方。CPU全程只参与计算,最终结果是什么含义,并不关心,就由存储设备本身自行解释。就像CPU往显卡上输出了数据,显卡器件自己知道要在屏幕上显示什么内容。

寻址

如果地址总线的宽度的表达范围 = 存储器的存储单元编号个数,地址总线就能容易地寻到任意的地址。但是8086中,一个存储器的寻址范围,可能需要20位总线才能找到所有地址。那么宽度为16的地址总线,怎样查找20位的内容呢。

16位的寻址范围为 0 ~ FFFF,20位的表示范围为 0 ~ F FFFF。在16进制数的表示下,一个20位的二进制比16位的二进制多了一个16进制位。

可以使用合成的方式来寻址,引入了概念:物理地址 = 段地址 + 偏移地址

物理地址的表示.png

段地址与偏移地址的共同表达形式,让同一物理地址拥有不同的表示方式。如同起点与终点有100米,“如果我站在起点处,我只需要走100米就可以到达终点” 与 “如果我站在距起点50米处,我只需要走50米就可以到达终点”,都能找到终点在哪。

回到CPU寻址上,对于段地址 A,偏移地址 B,物理地址 = A * 16 + B。 例如寻址 F FFFF时, A=F000和B=FFFF是一种表达方式,X=FFF0和Y=00FF也是一种表达方式。

这样,地址总线第一次的数据是段地址,第二次的数据是偏移地址,这样就能寻址了。

内存结构

存储器对于CPU来说都是内存,存储器本身可以简单地分为RAM(随机存储器)和ROM(只读存储器)。在汇编的视角上,RAM区域是任由我们使用的,但是ROM只能读写。原因在于,主板上ROM保存了BIOS(基本输入输出系统)的内容,通电后先运行里边的程序,它是一切的开始,并且是与外部器件连接的重要媒介。因此,里边的内容不应被改写。

认识一些寄存器

  • AX、BX、CX、DX:这个寄存器为一般寄存器,他们存于计算过程的中间数据的存储。同时为了兼容,每个16位的寄存器,可以进一步地分割为两个8位的寄存器。以 AX 为例,AX 可以进一步分为 AH 和 AL 分别表示 AX 的高8位和低8位。
  • CS与IP:段寄存器CS指明了程序代码的开始位置,任意时刻,CPU认为CS指向的位置为代码的开始位置。特别注意,我们现在是直接操作并参与计算过程和内存的访问过程的,没有通过操作系统进行操作时的约束,因此,需要有段寄存来指明某块内存区域的功用,在操作系统上时它已为我们屏蔽了这份信息并做了保护。IP寄存器则指明了下一个命令的开始位置,那么,程序的下一个命令的开始位置 = CS的值 * 16 + IP的值。
  • DS:段寄存器DS指明了数据段的位置,当要向内存单元读写信息时,读写的物理地址 = DS的值 * 16 + 输出的偏移地址。
  • SS和SP:内存中可以划分一段区域来完成栈的存储过程,段寄存器SS表示了这段区域的起始位置,专用寄存器SP表示了偏移范围,即栈顶位置 = SS的值 * 16 + SP的值。 需要注意的是,栈的增长是向低位地址方向增长的,如 SP = 0012H 时, 向栈中 push 2Byte 的数据,那么SP = 0012H - 0002H = 0010H 。栈头指向了更低位的地址单元。

认识字单元

除了存储单元来表示单位外,有事为了需要,还会使用字单元来表示一个存储单位,一个字单元占2个存储单元。

字单元.png

我们阅读一个数的习惯是从高位往低位阅读,如寄存器AX上的值也是按照 “1234” 的顺序来进行阅读。一个寄存器的数据容量刚好是一个字单元的容量,在存储时,寄存器的高位内容存在内存地址高位处,寄存器的低位内容存储在内存地址低位处,如 AH = 12 存在位置1,AL = 34存于位置0,位置0和位置1组成了一个2字单元。

认识汇编指令

汇编语言的初衷,是为了更好地使用机器指令完成计算工作。对于每条机器指令,对会有对应的汇编指令对应。

考虑一个操作,将寄存器BX的内容送到AX中:

机器指令:1000 1001 1101 1000
汇编指令:mov ax,bx

对比查看,机器指令哪以理解并容易出错,密密麻麻的0与1对于要编写与阅读的人而言无异于灾难。汇编指令使用符号来助记更符合人类习惯。

容易想见,汇编语言就是使用汇编指令构建出的程序过程。对于下面的内容,我们会使用到的指令有:

  • mov:表达将内容从一个地方移动到另一个地方。
  • add:表示将某些内容做加法,然后存在某个地方。
  • sub:表示将某些内容做减法,然后存在某个地方。
  • jmp:表示无条件跳转到某个位置。

每一种命令都有各自支持的各式,在使用到时加以说明。

环境安装

有了上面的知识,可以更直观地观察CPU的计算工作了。现在,我们需要安装8086环境以编写汇编。

可以用虚拟机安装 window2000,也可以去 DOSBox官网 下载对应的模拟器,然后自行安装所需要的插件。 Mac下可以参考 Mac下安装DOSBOX

安装完后,如果是虚拟机,按照:

开始 ->  运行 ->  command 
就可以唤起DOS窗口
输入 debug 进入目标界面

如果是DOSBox,运行,按照:

输入 “mount c  ‘你任意选的一个路径’ " 来挂载作为DOSBox的工作盘
输入 “c:” 进入这个挂载的路径
输入 “debug” 进入目标页面

Debug命令

在Debug页面下,有一些高频的命令可以查看寄存器与内存信息,并在后续使用时进一步说明:

  • R:查看、改变CPU寄存器的值
  • D:查看目标内存中的内容
  • E:改写目标内存中的内容
  • U:将目标内存中的内容,翻译成汇编指令
  • T:执行一条机器指令
  • A:将输入的汇编指令,翻译成机器指令,并写到内存中

r查看、改变寄存器内容

输入 r 查看寄存器情况

--输入r查看寄存器情况.jpg

前两行输出列出了当前所有寄存器上的值内容。红线部分为 CS:IP ,前面说过这两个寄存器表示了下一个指令的开始位置,段地址=073F,偏移地址=0100 ,后面均以这样的方式来描述一个地址。后面的0000是指所指向的命令的机器码,并在蓝线部分显示它对应的机器指令。

// 改写ax的值
输入 rax
输入 1200

// 查看寄存器情况
输入 r

--r改写ax的值.jpg

可以看到已经生效,其他的寄存器也可以以相同的方式使用Debug命令改写。

d 常看内存中的内容

输入d1000:0 查看内存地址上的内容

--D查看内存地址上的内容.jpg

上面的命令访问物理地址 10000H开始的内容,在没有指明范围的情况下,默认显示128个存储单元的内容。左边为地址信息,中间为对应的机器码(每行16个存储单元),右边为对应的ASCII码翻译。(我的图上的内存可以认为是没有什么内容的,因为我使用的是DOSBox并挂载到了一个干净的目录。)

比如,可以输入 “d1000:0 f”,这样就指定限制从物理地址 10000H 开始共16个存储单元的内容

--d显示16个存储单元的内容.jpg

e 改写内存中的内容

输入 e1000:0 12 00 'hello world' 00 34 

e的命令格式为 “e段地址:偏移地址 以空格隔开的字符串货数值”,命令执行后,从10000H出开始写入内容。

输入 d1000:0 查看内存中的内容

--e改写内存中的内容.jpg

已经生效。

a 将输入的汇编指令翻译成机器码,并写入内存

机器指令都有对应的汇编指令,但机器码是难以记忆的,可以使用a命令来以汇编命令的形式一次向内存中写入多个命令

输入 a1000:10
mov bx,ax
mov bl,ah
// 查看写入的内容
输入d1000:10  

mov 寄存器1,寄存器2 命令的含义,是将寄存器2的内容移动到寄存器1上,mov命令支持的格式为:

  • mov 寄存器1,寄存器2 : 将寄存器2的内容移动到寄存器2
  • mov 寄存器,数据 :将数据移动到寄存器上
  • mov 寄存器 内存单元 :将内存单元的内容移动到寄存器上
  • mov 内存单元,寄存器 :将寄存器上的内容移动到内存单元上
  • mov 段寄存器,寄存器: 将寄存器上的内容移动到段寄存器上

--a向内存中写入命令.jpg

a命令已将输入的汇编指令翻译成对应的机器码,写入到物理地址10010H开始的内存上。

u 命令查看目标内存的机器码对应的汇编指令

输入 u1000:10 查看刚才以 a命令写入的汇编指令

--u查看汇编指令.jpg

如果不指定数量的话,u命令默认翻译16条机器指令。可以看到物理地址 10010H 和 10012H 开始的机器码内容就是刚才我们以 a 命令写入的汇编指令。

t 执行下一条执行

要执行指令时,CPU会执行 CS:IP处指向的指令。当前CS:IP并没有指向我们以a命令写入的指令处。

输入 -r // 查看寄存器信息
输入 rcs
1000
输入 rip
10
输入 -r // 查看寄存器信息

--t改写命令指向处.jpg

CS:IP已指向我们写入的 "mov bx, ax" 指令,绿线处也显示出了对应的内容

输入 r 查看寄存器情况
输入 t 执行指令
输入 t 执行指令

--t执行指令.jpg

蓝箭头为每个指令执行后 BX 的值改变情况,红箭头为每个命令执行后 IP 自动改变偏移值指向下一命令的情况。

第一个命令 “mov bx, ax” 是将 ax 的值移动到 bx ,bx 的值由 0000 变成了 1200。 第二个命令 “mo bl, ah” 是将 al(ax高8位的值),移动到 bl(bx的低8位)上,al=12,因此命令指令后 bx 从 1200 变成了 1212。

更多的汇编命令解释

在有了Debug窗口的帮助下,已经可通通过查看汇编命令的格式语义,观察CPU的执行情况。现在,取出一些要特别说明的点,来让观察过程更顺利一些。

向内存单元的读写

观察mov命令格式 —— “mov 寄存器 内存地址” ,是内存地址上的内容写到寄存器中,如格式例子有如 “mov ax [2]”的形式。前面说过,段寄存器DS用来指向数据段的地址,那么在这个例子中,就从 DS:2的开始的内存地址中去一个字单元存入ax中,不难知道 [2] 中的数字指的是偏移地址。

现在,我们实现这个例子:

// 写入汇编命令到 10020H 处
a1000:20
mov ax, [2] 
// 让CS:IP指向这条命令
rcs
1000
rip
20
// 查看内存情况
d1000:00 2f

--内存单元读写.jpg

对应命令的机器码已被写入目标内存地址中。

// 目的是要从 10002H 处读取值,对应字单元的内容是 68 65
rds
1000
// 查看命令执行前寄存器的情况
-r
// 执行命令
-t

--内存单元读写命令执行.jpg

目标地址 1000:2 上的字单元内存 6865 已经写入到寄存器ax,ax=6568。别忘记字单元存储时,高位内容存在内存地址高位,低位内容存在内存地址低位。

操作栈

SS:SP可以指定一段内存区为作为栈使用,并且当栈增长时,向低位地址增长。

// 写入栈操作命令
a 1000:30
push bx
push ax
pop ax
// 查看写入的内容
d 1000:3f

--写入栈操作命令.jpg

命令已被写入从 10030H 开始的地方。

// 下一命令指向1000:30
rcs 
1000
rip
30
// 规划 10040H ~ 1004FH 为栈空间,别忘了,SP指向栈顶
rss
1004
rsp
f
// 执行 push bx
t
// 执行 push ax
t

--执行栈push操作.jpg

两个命令分别将 bx、ax的值写入栈中

// 查看寄存器内容
-r
// 查看栈的内容
- d1003:0 2f
// 执行出栈命令 
-t
// 再次查看栈内容
- d1003:0 2f

--执行栈操作命令.jpg

push操作执行后,bx和ax的内容都被存到了栈中,因为存入了两个字单元,栈顶位置SP由F变成了B。 在执行pop bx出栈操作后,栈顶内容顺利移动到BX上,SP也移动了一个字单元位置,由B变成了D

其他注意事项

虽然我们可以在Debug窗口,通过r命令直接改写寄存器的值。但是如 “mov 寄存器 数据”格式的命令,只能直接改写通用寄存器的值,一些寄存器则不能。因此在执行汇编命令改写其他寄存器的值时,需要通过 “mov 通用寄存器,数据来源” -> “mov 其他寄存器 通用寄存器” 的方式来改写。

一个预留的练习

在有了上面知识后,查询汇编命令的语义,就能观察其执行过程,现在,如有兴趣的可以自行通过一个例子检验每一步的情况是否符合我们的预期,来佐证是否读懂了文章。

// 清理出一片干净的内存区域,方便自行观察内存情况
 e 1000:50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 e 1000:60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 e 1000:70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 e 1000:80 12 34 56 78 1A 2B 3C 4D 00 00 00 00 00 00
 
 // 输入指令
 a 1000:50
 mov ax, 1080
 mov ds, ax
 mov ax, 0000
 mov sp, f 
 mov ax, [0]
 add ax, [2]
 mov bx, [4]
 add bx, [6]
 push ax
 push bx
 pop ax
 pop bx
 push [4]
 push [6]
 
 // 修改必要的寄存器指向
 CS -> 1000 , IP -> 50, SS -> 1070

注意哦:

  • add 的格式与mov类似,是将两者的内容相加,并存在前者的位置。
  • 如果存储单元的上的内容相加后,最高位溢出的话是不会进位的哦,直接丢弃。

总结

文章主要说明了在学习汇编语言前,所应掌握的基础知识,包括:

  • 在计算机中,信息以0和1的二进制存储,为了方便阅读,可以以更高的进制位如16进制进行转换。
  • 每个存储单元占8个二进制,即1字节。每个字单元占2个存储单元。
  • 每个机器指令,有对应的汇编指令来表示,更贴近人类阅读,书写习惯。
  • CPU的工作,是完成 y=f(x),为了完成工作,需要有存储设备来记录程序、数据源、计算结果。而在计算过程中,需要寄存器来保存中过程中的必要的辅助信息。
  • 每个寄存机占16位,通用寄存器可以进一步地分割为两个8位的寄存器。
  • CPU与内存以总线来交换信息,总线的信息传输能力取决于总线线宽。
  • 借助Debug的窗口命令,可以查阅内存与寄存器的情况,以观察CPU的计算过程。

当前,如想先行实验,其他的命令可自行查询。

文章大部分知识点来自于《汇编语言》第四版—— 王爽,第1-3章

参考

《汇编语言》第四版—— 王爽,第1-3章