本文节选自《计算机是怎样跑起来的(第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 Reference(http://ref.x86asm.net/coder32.html),然后搜索 mov
。
可以看到,同一条 mov
指令,却对应了 88
~8C
等多个十六进制数(这样的十六进制数称为Primary Opcode)。那 mov ebp, esp
中的 mov
到底对应哪个十六进制数呢?这就取决于操作数的类型了。操作数 ebp
和 esp
都是 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]
呢?
其实答案很简单,因为没有两个操作数都是内存地址这样的进行加法运算的指令。汇编语言的指令和机器语言的指令是一一对应的,如果某种机器语言的指令不存在,自然也就没有与之对应的汇编语言的指令了。或者说,如果找不到对应的机器语言的指令,这样的汇编语言的指令就是无效的。
在下一篇文章中,我们再来看一看如何查看寄存器和内存存储单元中的数据,以及如何进行逐行调试。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。