前言
已经好久没有发表文章了,对自己的要求没有达到,现在一点一点弥补过来。最近在了解操作系统是如何被开发出来的,发现如果对计算机的底层原理不熟悉的话,真的很难再看下去,所以回去看了一下汇编语言的基本知识。本文主要参考“小甲鱼”的课件,放在最后章节“参考资料”了,他的课件真的很不错,通俗易懂。我在这里简单地记录一下汇编语言的学习过程。
概念
汇编语言是一门底层的编程语言,可以直接操作计算机的硬件完成工作,所以学习汇编语言之前一定要对计算机的硬件有个基本的了解,但本文不打算介绍硬件相关的。
那么汇编语言是怎么工作的呢?因为计算机只知道0或者1,所以汇编语言需要经过汇编编译器编译之后,生成机器码供计算机使用。一些静态的编译型语言,往往会经过很多轮的编译和优化,最终转化为汇编程序,然后再会汇编编译器编译成机器码。
发展历史
- 机器语言(1940年代-1950年代)。在汇编语言出现之前,程序员们将 0、1 数字编程的程序代码打在纸带或卡片上,1打孔,0不打孔,再将程序通过纸带机或卡片机输入计算机,进行运算。这个就是机器语言。
- 汇编语言(1950年代-1960年代)。可是机器语言不好记住,于是程序员们开发了汇编语言,汇编指令是机器指令的助记符。16位的汇编语言是最经典的,虽然现在的计算机已经64位了,但是底层的实现原理没有太多的变化。所以本用也是基于16位计算机介绍汇编语言的。
- 高级语言(1960年代-1970年代):随着高级语言的兴起,汇编语言的使用逐渐减少。不过,一些特定领域,如嵌入式系统和操作系统,仍然需要使用汇编语言进行编程。
使用领域
尽管现在汇编语言的使用范围已经变得比较有限,但它仍然在以下几个领域中有广泛的应用:
- 嵌入式系统领域:嵌入式系统通常需要高效、精确的控制,而汇编语言可以提供更好的控制能力和性能优势。因此,在嵌入式系统的开发过程中,汇编语言仍然是一个重要的编程工具。
- 操作系统领域:操作系统需要直接与硬件进行交互,而汇编语言可以提供更好的底层编程能力。因此,在操作系统的内核开发过程中,汇编语言仍然扮演着重要的角色。
- 反向工程领域:反向工程是指通过分析二进制程序来获取程序的源代码和运行原理等信息。在反向工程的过程中,汇编语言是一个重要的工具,因为它可以帮助分析人员更好地理解程序的底层实现。
- 游戏开发领域:游戏开发需要高性能和精确的控制能力,而汇编语言可以提供更好的性能和控制能力。因此,在一些高性能的游戏开发中,汇编语言仍然是一个重要的编程工具。
学习收益
我总结出一下的我觉得有收益之处:
- 提高自己的编程思想。能够从机器的角度去编程,往往能够获得更高效率的代码。
- 作为高级语言的优化手段。在某些需要高效的场景下,高级语言的性能往往无法满足需求。此时,使用汇编语言编写一部分代码,可以显著提高程序的性能。很多高级语言的编译器和解释器也都会将高级语言转换成汇编语言来进行优化。
- 作为高级语言的调试工具。在某些情况下,高级语言的代码出现了问题,需要进行调试。此时,使用汇编语言可以更方便地查看内存状态、寄存器状态等底层信息,帮助程序员快速定位问题。
第一章(基本知识)
1. 汇编语言的组成
在汇编语言中,指令是不区分大小写的。
- 汇编指令(机器码的助记符),如mov、add、xor
- 伪指令 (由编译器执行),如assume、end
- 其它符号(由编译器识别),一些标号,加减乘除
2. 指令和数据
指令和数据是应用上的概念,CPU想要把当前指令就是指令,想要把你当成数据就是数据。
3. 总线
CPU、内存、外设之间的通信是通过内部总线,内部总线分为三种。
- 地址总线。决定CPU的寻址能力,x86是20位的地址总线,可寻址1MB。
- 数据总线。决定CPU和其他设备一次性传输的数据量。
- 控制总线。决定CPU向其他设备的控制能力。
-
内存地址空间
- 主存储器地址空间:由操作系统控制,可分配给程序使用。
- 显存地址空间:存放一些要在显示器显示的数据,固定存放在这里。
- BIOS地址空间:ROM是只读存储器,计算机加电后,各个设备的BIOS就会加载到内存的固定位置。
x86的地址总线是20位,所以可寻址的范围是0~1M,每个内存单元位1个字节。一般用32位来表示内存的地址,用16进制来表示,如001000FFH。
为了方便,CPU会根据(段地址+基地址)来定位一个物理地址。段(Segment)地址我们一般用段寄存器来存储,基地址一般用通用寄存器来存储。遵循下面的公式,物理地址 = 段地址*16 + 基地址,也相当于段寄存器左移4位,再加上基地址寄存器。如段地址为0001H,基地址位00FFH,物理地址则为001000FFH。
5. 寄存器
在x86架构的汇编语言中,有多个寄存器可以使用。这些寄存器可以分为通用寄存器、段寄存器和特殊寄存器等几类。以下是各个寄存器的介绍:
1. 通用寄存器
通用寄存器是用于存储数据的寄存器,可以在不同指令之间传递数据。x86架构的通用寄存器有8个,分别为AX、BX、CX、DX、SI、DI、BP、SP。其中,AX寄存器可以分为AH和AL两个部分,用于存储字节的高4位和低4位。各个通用寄存器的作用如下:
- AX寄存器:用于存储累加器(Accumulator),通常用于算术和逻辑操作。
- BX寄存器:用于存储基址(Base),通常用于内存寻址。
- CX寄存器:用于存储计数器(Counter),通常用于循环和字符串操作。
- DX寄存器:用于存储数据寄存器(Data),通常用于输入输出等操作。
- SI寄存器:用于存储源索引(Source Index),通常用于字符串操作。
- DI寄存器:用于存储目的索引(Destination Index),通常用于字符串操作。
- BP寄存器:用于存储基址指针(Base Pointer),通常用于栈操作。
- SP寄存器:用于存储栈指针(Stack Pointer),通常用于栈操作。
2. 段寄存器
段寄存器是用于存储内存段的地址的寄存器。x86架构的段寄存器有4个,分别为CS、DS、ES、SS。各个段寄存器的作用如下:
- CS寄存器:用于存储代码段的起始地址(Code Segment)。
- DS寄存器:用于存储数据段的起始地址(Data Segment)。
- ES寄存器:用于存储附加数据段的起始地址(Extra Segment)。
- SS寄存器:用于存储堆栈段的起始地址(Stack Segment)。
3. 特殊寄存器
特殊寄存器是用于特定目的的寄存器。x86架构的特殊寄存器有2个,分别为IP和FLAGS。各个特殊寄存器的作用如下:
- IP寄存器:用于存储指令指针(Instruction Pointer),指向当前执行的指令。
- FLAGS寄存器:用于存储标志位(Flags),用于存储运算结果的状态信息(如进位标志、零标志、符号标志等)。
6. 两条常用指令
分别是mov和add,指令很简单,不多做解释。
7. CS和IP
CS和IP联合使用,指向下一次要执行的指令地址。
- 一开始,CS指向2000H,IP指向0000H
- 物理地址则指向20000H,执行
mov ax,0123H - 执行后,因为上面的指令长度是3,则IP自动+3,指向2003
- 此时物理地址指向20003H,执行
mov bx,0003H
8. 代码段
上面讲过,我们使用(段寄存器+基寄存器)来表示物理地址,寄存器都是16位,则一个段最多表示64K的地址。比如下面这段程序,总共是10个字节,我们把它装在0001H:0000H~0001H:0009H,对应物理地址10001H~10009H,这段地址称之为代码段。段地址位0001H,段基址为0000H,段长度为10。
我们再来学习一个指令,jmp,用来跳转程序的执行,这个命令有一下几种形式。
- jmp 寄存器:CS不变,IP修改为寄存器的内容。
- jmp 段地址:偏移地址:把CS修改为段地址,把IP修改为偏移地址。
9. DS和[address]
CPU要读写内存时,必须指定内存单元的地址。[N]中的N代表偏移量,和DS寄存器配合使用。
mov bx,1000H
mov ds,bx
mov al,[0] ; 等同于mov al,[ds:0],传输一个字节(看寄存器的位数)
mov ax,[0] ; 等同于mov ax,[ds:0],传输一个字(看寄存器的位数)
add ax,ax ; ax中的值乘以2
mov [0],ax ; 等同于mov [ds:0],ax,将ax中的内容写入内存
因为段寄存器是不能直接赋值的,所以第1,2行借用ax将ds赋值。
第三行将内存为1000H:0H的内存读取一个字节到al中。[adress]是偏移量,默认的段是DS。
10. 数据段
区分代码段,表示段内存储的不是指令而是数据。通常用DS寄存器来存放数据段地址。
11. 栈
栈一种常用的数据结构,后进先出,汇编语言提供入栈(push)和出栈(pop)的指令。每次操作栈,都是以一个字为单位的。用SS寄存器存放栈段地址,用SP寄存器存放栈偏移量,任意时刻,SS:SP指向栈顶元素。内存中的栈是从高地址往低地址推的,所以栈顶反而在低地址。
; SS=1000H,SP=00F0H,对应图1(此时栈是空的)
mov ax,0123H
push ax ; SS=1000H,SP=000EH,对应图2
mov bx,2266H
push bx ; SS=1000H,SP=000CH,对应图3
mov cx,1122H
push cx ; SS=1000H,SP=000AH,对应图4
pop ax ; SS=1000H,SP=000CH,对应图5
pop bx ; SS=1000H,SP=000EH,对应图6
pop cx ; SS=1000H,SP=00F0H,对应图7
总结一下:
- push ax的执行:SP=SP-2,向SS:SP指向的字单元中送入数据。
- pop ax的执行:从SS:SP指向的字单元中读取数据,SP=SP+2。
push、pop指令除了指定寄存器,也可以指定偏移量。
push [0] ; 将ds:0的字压栈
pop [2] ; 将出栈后的数据写到ds:0中