第四章 机器语言
导读
前三章我们构建了计算机的硬件基础:逻辑门、算术逻辑单元和存储元件。本章将跨越硬件与软件的界限,介绍机器语言——计算机能够直接理解和执行的最低级语言。
机器语言是计算机硬件和软件之间的接口。理解机器语言对于理解计算机如何工作至关重要。通过本章的学习,你将掌握Hack计算机的机器语言,能够编写直接控制硬件的程序。
本章的学习目标包括:理解机器语言的概念和特点,掌握Hack机器语言的指令格式,学会使用机器语言编写基本程序,理解程序执行的过程。这些知识将为后续章节中构建汇编器和编译器奠定基础。
核心概念详解
4.1 机器语言概述
机器语言是计算机能够直接执行的指令集。每条指令都是一个二进制模式,告诉计算机执行特定的操作。
机器语言的特点
硬件相关性:每种计算机都有自己的机器语言,由硬件设计决定
低级性:直接操作硬件资源(寄存器、内存、ALU)
二进制表示:指令用二进制编码,难以被人阅读和编写
高效性:执行速度快,不需要翻译
指令集架构(ISA)
指令集架构定义了机器语言的规格,包括:
- 指令格式:指令的位布局和字段含义
- 数据类型:支持的数据类型和大小
- 寄存器:可用的寄存器及其用途
- 寻址模式:如何指定操作数地址
- 指令类型:支持的操作类型
4.2 Hack计算机架构
Hack是Nand2Tetris课程中使用的简化计算机架构。了解Hack架构是理解其机器语言的前提。
Hack计算机的组成
Hack计算机包含以下主要组件:
- ALU:16位算术逻辑单元
- 寄存器:三个16位寄存器(D、A、M)
- 内存:统一的16位内存空间
- 程序计数器(PC):15位程序计数器
Hack的寄存器
D寄存器(Data):
- 用于存储操作数
- 可以存储ALU的第二个输入
- 可以存储ALU的输出
A寄存器(Address):
- 用于存储地址或操作数
- 提供内存访问的地址
- 提供ALU的第一个输入
M寄存器(Memory):
- 不是物理寄存器
- 表示A寄存器指向的内存位置的内容
- M = RAM[A]
4.3 Hack机器语言指令格式
Hack机器语言有两种指令格式:A指令和C指令。
A指令(Address Instruction)
A指令用于将值加载到A寄存器:
指令格式:0vvvvvvvvvvvvvvv
|_____________|
15位地址/值- 最高位为0,标识这是A指令
- 低15位是要加载到A寄存器的值
- 可以加载0到32767之间的任何值
示例:
A指令:0000000000000100
含义:A = 4A指令的用途:
将数据加载到A寄存器
设置内存访问的地址
设置跳转的目标地址
C指令(Control Instruction)
C指令用于控制ALU执行操作,并可选地存储结果和执行跳转:
指令格式:111accacccdddjjj
|||_______________
|||15位未使用(固定为111)
|||
||a字段(1位)
|c字段(6位):ALU控制信号
|
a字段(1位):ALU输入选择
具体格式:111 a c1c2c3c4c5c6 d1d2d3 j1j2j3各字段含义:
a字段(1位):
- a=0:ALU的y输入来自A寄存器
- a=1:ALU的y输入来自M(内存)
c字段(6位):控制ALU执行的操作
c1c2c3c4c5c6 功能
000000 未使用
101010 out = x (A寄存器)
111111 out = 0
111010 out = -1
001111 out = x (重复)
110000 out = y (A或M)
001111 out = x
110001 out = -x
001111 out = x
110010 out = y (重复)
001111 out = x
110111 out = x-y
000011 out = y-x
000111 out = x+y
010101 out = x-y (重复)
011111 out = x AND y
011111 out = x OR y实际上,c字段的编码对应ALU的6个控制信号(zx, nx, zy, ny, f, no)。
d字段(3位):指定结果存储位置
d1d2d3 目标
000 不存储
001 M (内存)
010 A (A寄存器)
011 A,M
100 D (D寄存器)
101 D,M
110 D,A
111 D,A,Mj字段(3位):指定跳转条件
j1j2j3 条件
000 不跳转
001 jump if out > 0 (JGT)
010 jump if out = 0 (JEQ)
011 jump if out >= 0 (JGE)
100 jump if out < 0 (JLT)
101 jump if out != 0 (JNE)
110 jump if out <= 0 (JLE)
111 无条件跳转 (JMP)跳转是基于ALU输出的符号和零标志进行的。如果条件满足,PC被设置为A寄存器的值。
4.4 Hack指令集详解
基本操作
数据传送:
将值加载到A:@value (A指令)
D = A:111 0 111111 010 000
D = M:111 1 110000 010 000
A = D:111 0 001100 100 000
M = D:111 0 001100 001 000算术运算:
D = D + 1:111 0 011111 010 000
D = D - 1:111 0 001110 010 000
D = D + A:111 0 000010 010 000
D = D - A:111 0 010011 010 000
D = A - D:111 0 000111 010 000
D = D AND A:111 0 000000 010 000
D = D OR A:111 0 010101 010 000内存访问:
D = M:111 1 110000 010 000
M = D:111 0 001100 001 000
D = M + 1:111 1 110111 010 000
D = M - 1:111 1 110010 010 000控制流
无条件跳转:
(A)
@label
111 0 111000 000 111条件跳转:
@label
D;JGT // 如果D>0,跳转到label
D;JEQ // 如果D=0,跳转到label
D;JLT // 如果D<0,跳转到label4.5 程序执行过程
理解程序执行过程对于编写正确的机器语言程序至关重要。
取指-执行周期
Hack计算机的执行过程遵循经典的取指-执行周期:
取指(Fetch):
- PC提供下一条指令的地址
- 从内存中读取指令
- PC递增
执行(Execute):
- 解码指令
- 执行指令指定的操作
- 更新寄存器和内存
指令执行细节
A指令执行:
将指令的低15位加载到A寄存器
PC递增
C指令执行:
根据a字段选择ALU的y输入(A或M)
根据c字段设置ALU控制信号
ALU执行计算
根据d字段存储结果
根据j字段和ALU输出决定是否跳转
如果跳转,PC=A;否则PC递增
4.6 编程示例
示例1:计算1+2+...+10
// 初始化
@10 // A = 10 (循环计数)
D = A // D = 10
@0 // A = 0
M = D // M[0] = 10 (保存计数)
@0 // A = 0
M = M // 重新加载
@sum // A = sum的地址
M = 0 // sum = 0
// 循环
(loop)
@10 // A = 10
D = M // D = 计数
@END // A = END的地址
D;JLE // 如果计数<=0,跳转到END
@sum // A = sum的地址
D = D + M // D = 计数 + sum
M = D // sum = D
@10 // A = 10
D = M // D = 计数
D = D - 1 // D = 计数 - 1
@10 // A = 10
M = D // 计数 = D
@loop // A = loop的地址
0;JMP // 无条件跳转到loop
(END)
@END // A = END的地址
0;JMP // 无限循环示例2:数组求和
// 计算数组[100..109]的和
@100 // A = 100 (数组起始地址)
D = A // D = 100
@i // A = i的地址
M = D // i = 100
@sum // A = sum的地址
M = 0 // sum = 0
(LOOP)
@i // A = i的地址
D = M // D = i
@110 // A = 110 (数组结束地址+1)
D;JGE // 如果i>=110,跳转到END
@i // A = i的地址
A = M // A = i
D = M // D = M[i]
@sum // A = sum的地址
M = M + D // sum = sum + M[i]
@i // A = i的地址
M = M + 1 // i = i + 1
@LOOP // A = LOOP的地址
0;JMP // 跳转到LOOP
(END)
@END
0;JMP4.7 输入输出
Hack计算机使用内存映射I/O:
- 内存地址16384-32767:屏幕内存
- 内存地址24576:键盘内存
屏幕输出
屏幕是512×256像素的黑白显示器,每个像素用1位表示(0=黑,1=白)。
屏幕内存映射:
- 每个内存字对应屏幕上的32个水平像素
- 内存地址16384对应屏幕左上角
- 内存地址24575对应屏幕右下角
示例:点亮屏幕左上角的像素
@16384 // A = 屏幕内存起始地址
M = 1 // 设置第一个字为1,点亮第一个像素键盘输入
键盘通过内存地址24576访问:
- 没有按键按下时,值为0
- 有按键按下时,值为按键的ASCII码
示例:等待按键
(WAIT)
@24576 // A = 键盘地址
D = M // D = 按键值
@WAIT // A = WAIT的地址
D;JEQ // 如果D=0(无按键),继续等待
// D现在包含按键的ASCII码重要知识点
知识点1:指令编码的效率
机器语言指令的编码需要在功能和长度之间取得平衡。Hack使用固定长度(16位)指令,简化了硬件设计,但限制了指令的功能。
固定长度指令:
- 优点:硬件设计简单,解码快速
- 缺点:功能受限,代码密度低
可变长度指令:
- 优点:功能丰富,代码密度高
- 缺点:硬件设计复杂,解码慢
知识点2:寻址模式
寻址模式指定如何获取操作数。Hack支持有限的寻址模式:
立即寻址:操作数在指令中(A指令)
寄存器寻址:操作数在寄存器中
直接寻址:操作数在内存中,地址在A寄存器
间接寻址:通过寄存器中的地址访问内存
知识点3:程序计数器的重要性
程序计数器是控制流的关键。通过修改PC,可以实现:
- 顺序执行:PC自动递增
- 跳转:PC被设置为指定地址
- 条件分支:根据条件修改PC
- 循环:通过跳转实现
知识点4:内存映射I/O
内存映射I/O将I/O设备映射到内存地址空间:
- 优点:统一的访问方式,简化指令集
- 缺点:占用内存地址空间
知识点5:机器语言编程的挑战
机器语言编程非常繁琐且容易出错:
- 需要手动管理寄存器和内存
- 需要使用数字地址而不是符号名称
- 需要手动计算跳转目标
- 代码难以阅读和维护
这些挑战促使了汇编语言和高级语言的发展。
常见误区
误区1:混淆A寄存器和M
A寄存器存储地址,M表示该地址指向的内存内容。M = RAM[A],不是独立的寄存器。
误区2:不理解C指令的字段
C指令的字段看似复杂,但每个字段都有明确的作用。理解每个字段的编码规则对于正确编写C指令至关重要。
误区3:忽视跳转条件的细节
跳转是基于ALU输出的,而不是基于特定寄存器的值。理解ALU输出如何影响跳转条件很重要。
误区4:认为机器语言程序可以直接运行
实际上,机器语言程序需要加载到内存中才能执行。在Hack计算机中,程序从地址0开始加载。
误区5:过度使用寄存器
Hack只有三个可用寄存器(D、A、M),需要合理规划寄存器的使用。过度使用寄存器会导致频繁的内存访问,降低程序效率。
实践应用
实践1:编写简单的算术程序
计算(5+3)×2:
@5 // A = 5
D = A // D = 5
@3 // A = 3
D = D + A // D = 5 + 3 = 8
D = D + D // D = 8 × 2 = 16
@result // A = result的地址
M = D // result = 16实践2:实现条件判断
实现if-then-else结构:
// if (x > 0) y = 1; else y = 2;
@x // A = x的地址
D = M // D = x
@ELSE // A = ELSE的地址
D;JLE // 如果x <= 0,跳转到ELSE
@1 // A = 1
D = A // D = 1
@y // A = y的地址
M = D // y = 1
@END // A = END的地址
0;JMP // 跳转到END
(ELSE)
@2 // A = 2
D = A // D = 2
@y // A = y的地址
M = D // y = 2
(END)
// 继续执行实践3:实现循环
实现while循环:
// while (x > 0) x = x - 1;
(WHILE)
@x // A = x的地址
D = M // D = x
@END // A = END的地址
D;JLE // 如果x <= 0,退出循环
@x // A = x的地址
M = M - 1 // x = x - 1
@WHILE // A = WHILE的地址
0;JMP // 继续循环
(END)
// 继续执行实践4:实现数组操作
遍历数组并求和:
@array // A = 数组起始地址
D = A // D = 数组地址
@i // A = i的地址
M = D // i = 数组地址
@sum // A = sum的地址
M = 0 // sum = 0
@array_end // A = 数组结束地址
D = A // D = 数组结束地址
@end_addr // A = end_addr的地址
M = D // end_addr = 数组结束地址
(LOOP)
@i // A = i的地址
D = M // D = i
@end_addr // A = end_addr的地址
D;JGE // 如果i >= end_addr,退出
@i // A = i的地址
A = M // A = i
D = M // D = array[i]
@sum // A = sum的地址
M = M + D // sum = sum + array[i]
@i // A = i的地址
M = M + 1 // i++
@LOOP // A = LOOP的地址
0;JMP // 继续循环实践5:屏幕绘图
在屏幕上画一条水平线:
@16384 // A = 屏幕内存起始地址
D = A // D = 起始地址
@addr // A = addr的地址
M = D // addr = 起始地址
@32 // A = 32 (32个字 = 512像素)
D = A // D = 32
@count // A = count的地址
M = D // count = 32
(DRAW)
@count // A = count的地址
D = M // D = count
@DONE // A = DONE的地址
D;JLE // 如果count <= 0,完成
@addr // A = addr的地址
A = M // A = 当前地址
M = -1 // 设置所有32位为1(白色)
@addr // A = addr的地址
M = M + 1 // addr++
@count // A = count的地址
M = M - 1 // count--
@DRAW // A = DRAW的地址
0;JMP // 继续绘制
(DONE)
@DONE // A = DONE的地址
0;JMP // 无限循环实践6:键盘交互
等待按键并显示:
(WAIT)
@24576 // A = 键盘地址
D = M // D = 按键值
@WAIT // A = WAIT的地址
D;JEQ // 如果无按键,继续等待
// 按键值在D中
@16384 // A = 屏幕地址
M = D // 在屏幕上显示按键值
@WAIT // A = WAIT的地址
0;JMP // 继续等待本章小结
本章我们深入学习了Hack计算机的机器语言。从指令格式到程序执行,我们全面了解了机器语言的各个方面。
核心要点回顾
机器语言概念:计算机能够直接执行的最低级语言,由二进制指令组成。
Hack指令格式:A指令用于加载值到A寄存器,C指令用于控制ALU和程序流程。
指令执行:遵循取指-执行周期,PC控制指令顺序。
编程方法:使用A指令和C指令组合实现各种功能。
I/O操作:通过内存映射I/O访问屏幕和键盘。
关键技能掌握
- 理解并能够编写A指令和C指令
- 掌握机器语言程序的基本结构
- 能够实现条件判断和循环
- 能够进行内存访问和I/O操作
- 理解程序执行的过程
与后续章节的联系
本章学习的机器语言是后续章节的基础:
- 第六章将构建汇编器,将符号化的汇编语言转换为机器语言
- 第七章和第八章将介绍虚拟机,提供更高级的抽象
- 后续章节将在此基础上构建完整的软件栈
学习建议
动手实践:亲手编写和测试机器语言程序
理解细节:深入理解每条指令的编码和执行过程
循序渐进:从简单程序开始,逐步增加复杂度
思考抽象:思考如何从机器语言抽象出更高级的语言
通过本章的学习,你已经掌握了机器语言的核心知识。虽然直接编写机器语言程序非常繁琐,但这种低层次的理解对于理解计算机工作原理至关重要。正是为了克服机器语言编程的困难,人类发明了汇编语言和高级语言,使编程变得更加容易和高效。