本文节选自《计算机是怎样跑起来的(第2版)》第 3 章“体验汇编语言”的草稿。在翻译本章时,我们发现原书所使用的软件仅提供日文界面,并且介绍的是主要用于日本计算机相关考试的 CASLⅡ 汇编语言,其通用性相对较低。为了让内容更广泛适用,并便于读者实践操作,与作者及编辑老师商议后,决定采用更为通用的NASM 汇编语言重新编写本章,以提升学习体验。


通过前面的学习(用汇编语言编写计算两整数之和的程序(上)),我们知道,虽然同属于低级语言,但汇编语言的程序需要先转换成机器语言的程序才能由 CPU 解释执行。那“计算 1+2”这段代码对应着怎样的机器语言的代码呢?

查看汇编语言对应的机器语言

在汇编语言的开发调试工具 SASM 中,可以通过 GDB 命令来查看汇编语言对应的机器语言的代码。

点击工具栏中的“调试”按钮(图标是右下角有一只蓝色甲虫的绿色三角形)就会进入调试模式。此时,SASM 窗口底部的窗格中会出现一行绿色的文字“正在调试...”。同时,最底部还会出现一个名为“GDB 命令:”的输入框。

另外,进入调试模式后,SASM 会插入一行代码 mov ebp, esp。这行代码后面的注释 ;for correct debugging(为了正确的调试)说明这行代码仅用于辅助调试,并不会影响程序的运行,可以忽略,如下图所示。

我们依次输入如下两条 GDB 命令:

  • set disassembly-flavor intel
  • disassemble /r

按下回车键后,就会看到在底部的窗格中输出了大量信息:

> set disassembly-flavor intel

> disassemble /r
 Dump of assembler code for function main:
=> 0x00401390 <+0>:        89 e5                mov    ebp,esp
   0x00401392 <+2>:        a1 00 20 40 00        mov    eax,ds:0x402000
   0x00401397 <+7>:        03 05 04 20 40 00    add    eax,DWORD PTR ds:0x402004
   0x0040139d <+13>:    a3 08 20 40 00        mov    ds:0x402008,eax
   0x004013a2 <+18>:    e8 02 00 00 00        call   0x4013a9 <main+25>
   (略)

输出信息中中间部分的十六进制数就是机器语言的代码,例如,第一行中间部分的 89 e5 就是 mov ebp,esp 对应的机器语言的代码。那么,mov 指令对应的十六进制数就是 89 吗?为什么这条 mov 指令明明有两个操作数,应该对应两个十六进制数才对,却只对应了一个 e5?这些问题只能通过查询 CPU 的指令手册才能得知。

mov ebp,esp 对应的机器语言的代码

我们打开X86 Opcode and Instruction Referencehttp://ref.x86asm.net/coder32.html),然后搜索 mov

可以看到,同一条 mov 指令,却对应了 88~8C 等多个十六进制数(这样的十六进制数称为Primary Opcode)。那 mov ebp, esp 中的 mov 到底对应哪个十六进制数呢?这就取决于操作数的类型了。操作数 ebpesp 都是 32 位的寄存器,匹配 r/m16/32 r16/32 这个模式(第 1 个操作数是寄存器或 16/32 位的内存地址,第 2 个操作数是 16/32 位寄存器),所以这一行代码中的 mov 应该对应 89。【TODO 为什么不是 8B?】

e5 又是如何对应 ebp, esp 这两个寄存器的呢?这就又涉及到称为 ModR/M byte(https://en.wikipedia.org/wiki/ModR/M)的指令编码方式了。

+-------+---+---+---+---+---+---+---+---+
|  Bit  | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------+---+---+---+---+---+---+---+---+
| Usage |  MOD  |    REG    |    R/M    |
+-------+---+---+---+---+---+---+---+---+
|   e5  | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 1 |
+-------+---+---+---+---+---+---+---+---+

0xe5 = 1110 0101,如图所示,从左往右数,前两位的二进制数 11 是 MOD,MOD = 11 表示该指令的两个操作数都是寄存器。之后的连续三位二进制数 100 对应寄存器 esp,最后的三位二进制数 101 对应寄存器 ebp(参考http://ref.x86asm.net/coder32.html#modrm_byte_32)。这样一来,一个十六进制数就可以对应两个操作数了。

mov eax, [A]对应的机器语言代码

我们再来看下一行汇编语言的代码 mov eax, [A] 对应的机器语言的十六进制数

a1 00 20 40 00        mov    eax,ds:0x402000

继续在 X86 Opcode and Instruction Reference(http://ref.x86asm.net/coder32.html)中搜索 a1,可以看到

A1    MOV    eAX    moffs16/32

mov 指令(这次对应a1)的两个操作数分别是 eax 寄存器和 16/32 位的内存地址的偏移量(offset)。结合后面的 ds:0x402000 来看,00 20 40 00 就是内存地址偏移量(采用小端序),这里的 ds 是数据段 data segment 的缩写。另外,这个内存地址就是标签 A 对应的内存地址,可以通过 GDB 命令 p &A 验证这一点。

add eax, [B]对应的机器语言代码

最后再来看一下 add 指令对应的机器语言代码。

03 05 04 20 40 00    add    eax,DWORD PTR ds:0x402004

继续查询上述手册可知:

03   ADD    r16/32    r/m16/32

add 指令对应 03,后续的 05 又是ModR/M byte,表示该指令的第一个操作数是寄存器 eax(REG=000),第二个操作数是内存地址(R/M=101)。

+-------+---+---+---+---+---+---+---+---+
|  Bit  | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+-------+---+---+---+---+---+---+---+---+
| Usage |  MOD  |    REG    |    R/M    |
+-------+---+---+---+---+---+---+---+---+
|   05  | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
+-------+---+---+---+---+---+---+---+---+

05 之后的四个字节 04 20 40 00 是标签 B 对应的内存地址。

现在诸位应该能深切地感受到为什么要发明汇编语言了吧,因为使用机器语言编程时不得不记忆毫无规律的数字,实在是太不方便了。

上一篇文章的结尾处提出了一个问题:

在高级语言中,计算两整数之和可以只用两个变量 a += b,但在汇编语言中,为什么不能写成 add [A], [B] 呢?

其实答案很简单,因为没有两个操作数都是内存地址这样的进行加法运算的指令。汇编语言的指令和机器语言的指令是一一对应的,如果某种机器语言的指令不存在,自然也就没有与之对应的汇编语言的指令了。或者说,如果找不到对应的机器语言的指令,这样的汇编语言的指令就是无效的。


在下一篇文章中,我们再来看一看如何查看寄存器和内存存储单元中的数据,以及如何进行逐行调试。


da_miao_zi
1 声望0 粉丝

软件工程师、技术图书译者。译有《图解云计算架构》《图解量子计算机》《计算机是怎样跑起来的》《自制搜索引擎》等。