04

机器语言

CPU 能理解的指令

阅读量:6 · 已阅读:2秒 · 预计 9 分钟读完

指令集寻址模式汇编
阅读进度4%

第四章 机器语言

导读

前三章我们构建了计算机的硬件基础:逻辑门、算术逻辑单元和存储元件。本章将跨越硬件与软件的界限,介绍机器语言——计算机能够直接理解和执行的最低级语言。

机器语言是计算机硬件和软件之间的接口。理解机器语言对于理解计算机如何工作至关重要。通过本章的学习,你将掌握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 = 4

A指令的用途:

将数据加载到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,M

j字段(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,跳转到label

4.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;JMP

4.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操作
  • 理解程序执行的过程

与后续章节的联系

本章学习的机器语言是后续章节的基础:

  • 第六章将构建汇编器,将符号化的汇编语言转换为机器语言
  • 第七章和第八章将介绍虚拟机,提供更高级的抽象
  • 后续章节将在此基础上构建完整的软件栈

学习建议

动手实践:亲手编写和测试机器语言程序

理解细节:深入理解每条指令的编码和执行过程

循序渐进:从简单程序开始,逐步增加复杂度

思考抽象:思考如何从机器语言抽象出更高级的语言

通过本章的学习,你已经掌握了机器语言的核心知识。虽然直接编写机器语言程序非常繁琐,但这种低层次的理解对于理解计算机工作原理至关重要。正是为了克服机器语言编程的困难,人类发明了汇编语言和高级语言,使编程变得更加容易和高效。