【汇编】汇编系列一 - 内存

认识汇编对了解内存和分析底层的一些内容非常有帮助。

一、简介

1.1. 编程语言的发展

机器语言:由0和1组成。

汇编语言(Assembly Language):用符号代替了0和1,比机器语言便于阅读和记忆。

高级语言:C\C++\Java\Swift等,更接近人类自然语言。

如下图,将寄存器BX的内容送入寄存器AX,不同的语言所表示的方式也不同:

汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令。

汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言。

高级语言可以通过编译得到汇编语言\机器语言,但汇编语言\机器语言几乎不可能还原成高级语言。

1.2. 汇编语言特点

  • 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能。
  • 汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性。
  • 不区分大小写,比如mov和MOV是一样的。
  • 知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护。

采用高级语言C++和汇编语言编写同一个功能时,最终形成的可执行文件大小可能会相差很大(因为C++需要链接很多库)。例如,将a + b的结果赋值给c,然后在屏幕上打印c的结果:

1.3. 汇编语言的用途

为什么要学习汇编语言(它的用途是什么)?

  • 编写驱动程序、操作系统(比如Linux内核的某些关键部分)

  • 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)

  • 软件安全

    • 病毒分析与防治

    • 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客

  • 是理解整个计算机系统的最佳起点和最有效途径

  • 为编写高效代码打下基础(可以直接看到代码是如何操作内存的,进而反映出代码的执行效率)

  • 弄清代码的本质(很多理论和说法是错误的),例如:

    • sizeof是一个函数么?
    • ++a + ++a + ++a,这行代码的执行结果是什么?
    • switchif的效率究竟谁高?为什么?
    • ……

1.4. 汇编语言的种类

目前讨论比较多的汇编语言有:

  • 8086汇编(8086处理器是16bit的CPU)

  • Win32汇编

  • Win64汇编

  • AT&T汇编(Mac、iOS模拟器)

  • ARM汇编(嵌入式、iOS设备)

  • ……

入门建议先从学些8086汇编开始,因为8086汇编结构简洁、经典。参考书籍:王爽《汇编语言》。

二、内存

要想学好汇编语言,首先要对CPU和内存等硬件结构有一定的了解。在学习汇编语言过程中,遇到的绝大部分指令都是跟内存、CPU有关的。

如下图是软件\程序的执行过程:

2.1. 总线

每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互。

总线是一根根导线的集合,分为地址总线数据总线控制总线

内存由很多单元组成,每一个内存单元都有一个物理地址。如下图,CPU从内存的3号单元读取数据:CPU通过地址线找到内存的3号单元,再通过控制线获知当前的指令是向内存读取数据,此时通过数据线把内存的3号单元存储的数据返回到CPU。

数字电路中,把电压的高低用逻辑电平来表示。逻辑电平包括高电平和低电平这两种。计算机信号使用的是数字电路信号,由0和1组成,高电平代表1,低电平代表2。如下图,总线宽度(线路条数)是3,一根线只能发送0或1信号,3条线就可以发送$2^3$种组合信号。

地址总线:内存寻址

  • 它的宽度决定了CPU的寻址能力(能找到的最大地址)
  • 8086的地址总线宽度是20,所以寻址能力是1M( $2^{20}$ )

数据总线:数据传输

  • 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度
  • 8086的数据总线宽度是16,所以单次最大传递2个字节的数据(每条线代表1bit,16条线就是16bit,1B = 8bit)

控制总线:读写命令

  • 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制

8088的数据总线宽度是8,8086的数据总线宽度是16,分别向内存中写入89D8H(H是Hex的缩写,代表的是十六进制,是汇编语言中表示进制的一种方式,不区分大小写)。8088需要传送两次数据,而8086只需要一次就可以把数据传输完成。

思考与练习:

2.2. 存储器

各类存储器的逻辑连接情况:

所有的内存单元都有唯一的地址,叫做物理地址。

各类存储器的物理地址情况:

  • 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位$2^{20}$个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB。
  • 0x00000~0x9FFFF:主存储器。可读可写。
  • 0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写。
  • 0xC0000~0xFFFFF:存储各种硬件\系统信息。只读。

下图是各类存储器的逻辑连接 - 物理地址对应图:

2.3. 寻址方式

CPU访问内存单元时,要给出内存单元的地址。8086有20位地址总线,可以传送20位的地址,1M的寻址能力。但它又是16位结构的CPU,它内部能够一次性处理、传输、暂时存储的地址为16位。如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出来的寻址能力只有64KB。

8086采用一种在内部用2个16位地址合成的方法来生成1个20位的物理地址。

地址是用十六进制表示的,而16的十六进制是10,所以段地址 X 16转换成十六进制就是段地址 X 10H

2.4. 内存的分段管理

8086是用“起始地址(段地址×16) + 偏移地址 = 物理地址”的方式给出物理地址。为了开发方便,我们可以采取分段的方法来管理内存,比如:

  • 地址10000H~100FFH的内存单元组成一个段,该段的起始地址为10000H,段地址为1000H,大小为100H
  • 地址10000H1007FH、10080H100FFH的内存单元组成2个段,它们的起始地址为:10000H和10080H,段地址为1000H和1008H,大小都为80H

偏移地址为16位,16位地址的寻址能力为64KB,所以一个段的长度最大为64KB。

注意:8086是一个特例,在32bit和64bit电脑上,内存地址都是直接输出的。