---
schema_version: "library-reader-v1"
source_type: "original_book"
title: "编码 隐匿在计算机软硬件背后的语言"
author: null
edition: null
publisher: null
year: null
isbn: "9787121181184"
library_document_id: "38270854-596f-4a26-8183-db8c968c6020"
source_sha256: "249786274d83909f0bddc34d77f81ed8f0b14d1eac8843c772a37a3ecbfe2cfc"
pdf_physical_page_count: 438
pdf_physical_page_index_base: 0
range_start: 310
range_count: 10
html_url: "https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020?start=310&limit=10"
markdown_url: "https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020.md?start=310&limit=10"
previous_url: "https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020.md?start=300&limit=10"
next_url: "https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020.md?start=320&limit=10"
---

# 编码 隐匿在计算机软硬件背后的语言

> 页码规则：PDF_PHYSICAL_PAGE_INDEX 是从 0 开始的权威文件物理页索引；PRINTED_PAGE 是页面中识别出的印刷页码，可能为空，二者不得混用。

## PDF_PHYSICAL_PAGE_INDEX=310 | PRINTED_PAGE=294

寄存器B中的内容被保存到堆栈指针指向的地址，即存储器地址7FFFh处。

堆栈指针减1，变为7FFEh。

寄存器C中的内容被保存到堆栈指针指向的地址，即存储器地址7FFEh处。

类似的，在堆栈指针仍为7FFEh的情况下，执行POP BC指令时会将上面的步骤反过来执行一遍：

堆栈指针指向的地址（7FFEh）的内容加载到累加器C。

堆栈指针加1，变为7FFFh。

堆栈指针指向的地址（7FFFh）的内容加载到累加器B。

堆栈指针加1，变为8000h。

每执行一条PUSH指令，堆栈都会增加两个字节，这可能会导致程序出现一些小错误一堆栈可能会不断增大，最终覆盖掉存储器中保存的程序所必需的代码或数据。这种错误被称作堆栈上溢（stack overflow）。类似的，如果在程序中过多地使用了POP指令，则会过早地取完堆栈中的数据从而导致类似的错误，这种情况称为堆栈下溢（stackunderflow)。

如果8080连接的是一个64KB的存储器，你可能会把堆栈指针初始化为0000h。当执行第一条 PUSH指令时，堆栈指针会减1变为 FFFFh，即存储器的最后一个的存储单元。这时，堆栈的初始位置将会是存储器的最高地址，因为程序的代码通常从0000h开始存放，因此两者将保持非常远的距离。

8080 使用LXI指令为堆栈寄存器赋值，LXI是Load Extended Immediate的缩写，即加载扩展的立即数。下面的这些指令将把操作码后的两个字节保存到16位寄存器对中。

指令：

LXI BC, 527Ah

编码隐匿在计算机软硬件背后的语言

294

## PDF_PHYSICAL_PAGE_INDEX=311 | PRINTED_PAGE=295

和下面两条指令等价：

MVI B, 52h

MVI C, 7Ah

LXI指令保存一个字节。而且上表中最后一条LXI指令为堆栈指针赋了一个特殊的值。通常下面的指令不作为微处理器复位之后首先执行的指令之一。

0000h: LXI SP, 0000h

类似的，还可以对寄存器对和堆栈指针进行加1或减1操作，即把它们看做16位寄存器。

对于要讨论的16位指令，我们可以再看一些例子。下面的指令把由任意2个寄存器组成的16位寄存器对的内容加到寄存器对HL中。

这些指令可以减少操作的字节数。例如，上面的第一条指令一般情况下需要6个字节。

MOV A, L ADD A, C MOV L, A MOV A, H ADC A, B MOV H, A

DAD指令一般用来计算存储器地址，只对进位标志位CF有影响。

19 两种典型的微处理器

295

## PDF_PHYSICAL_PAGE_INDEX=312 | PRINTED_PAGE=296

接下来我们来认识一下各种各样的指令。下面两条指令的特点是操作码后面都跟着2字节的地址，第一条指令把HL寄存器对的内容保存到该地址，第二条指令把该地址的内容加载到HL寄存器对。

寄存器L的数据保存在地址 aaaa，而寄存器H的数据保存在地址 aaaa+1。

下面的两条指令把寄存器对HL保存的数据加载到程序计数器和堆栈指针。

PCHL指令本质上是一种 Jump指令，它把HL保存的存储器地址加载到程序计数器，而8080处理器要执行的下一条指令就是程序计数器所指明的存储器地址中存放的指令。SPHL指令可以作为另一种为堆栈指针赋值的指令。

下面的两条指令中，第一条将HL保存的数据与堆栈顶部的两个字节进行交换；第二条指令将HL保存的数据和寄存器对DE保存的数据进行交换。

除了刚讲过的PCHL指令外，目前为止还没有介绍过8080的跳转指令。如第17章所述，在处理器中专门设置了一个称为程序计数器PC的寄存器，它用来保存处理器将要取出并执行的指令的存储地址。通常，处理器在PC的指引下顺序执行存储器中存放的指令，但有一些指令，如Jump（跳转）、Branch（分支）或Goto（无条件转移）——使处理器脱离原来的执行顺序。这些指令使PC重新加载另外的值，处理器要执行的下一条指令存放于存储器的其他位置，而不在按原来的顺序寻址。

当然，原始的普通跳转指令的确有一定的作用，但从第17章得来的经验可以知道，条件跳转（conditionaljump）指令的作用更大。条件跳转指令使处理器根据某些标志位的值转移到特定的地址，这些标志位可以是进位标志位CF、零标志位ZF等。正是由于条件跳转指令的引入，第17章所设计的自动加法器才成为一般意义上的数字计算机。

编码—隐匿在计算机软硬件背后的语言

296

## PDF_PHYSICAL_PAGE_INDEX=313 | PRINTED_PAGE=297

8080有5个标志位，其中有4个可用于条件跳转指令。8080支持9种不同的跳转指令，包括了非条件跳转指令，还包含根据 ZF(Zero Flag)、CF(Carry Flag)、PF(Parity Flag)以及 SF（Sign Flag）是否为1而跳转的条件跳转指令。

在介绍这些指令之前，首先来介绍与Jump指令相关的另外两种指令。第一种是Call（调用）指令，它和Jump指令类似，但是有所不同的是：执行Call指令后，程序计数器(Program Counter，在这部分讲解中简称PC)加载一个新的地址，而处理器会把原来的地址保存起来，保存到何处呢？最好的选择自然是堆栈了。

这种策略使 Call 指令有效地记录了“从何处跳转”（where it jumped from)，即保存了跳转之前的相关信息。堆栈中保存的地址可以使处理器最后返回到转移之前的位置。用于返回的指令称为Return（返回）。Return指令从堆栈中弹出两个字节，并把它们加载到PC中，这样就完成了返回到跳转点的工作。

对于任何处理器来说，Call和 Return指令都非常重要。在它们的帮助下，程序员可以在程序中使用子程序（subroutine)，子程序是一段频繁使用的完成特定功能的代码（这里的“频繁”意味着“不止一次”)。对于汇编语言来说，子程序是其基本的组成部分。

让我们来看一个使用子程序的例子。假设你在编写一个汇编语言程序，在程序的某个位置你需要把两个字节相乘，因此你编写了一段用于两个数相乘的代码，然后继续向下写，在程序的另一个位置你发现需要再一次对两个字节相乘。因为你已经写过把两个字节相乘的代码，所以只需要重复使用这些代码就可以了。但是怎么做呢？只是简单地把这些代码重复输入到存储器吗？我们希望不是，因为这样做不仅耽误时间而且浪费存储空间，一个更好的方法是跳转到先前写的那段乘法代码所在的位置。但是普通的Jump指令不能完成这个操作，因为在执行乘法之后不能准确地返回程序的当前位置。因此你需要使用Call指令和 Return指令来帮助你实现这个功能。

用来实现两个数相乘的一组指令可以作为一个子程序。下面我们将看到这个子程序。在第17章的乘法程序中，被乘数（还有乘积）被保存在存储器的特定位置；而在8080的子程序中，乘数和被乘数分别存放在寄存器B和寄存器C中，乘积保存到16位寄存器

19 两种典型的微处理器

297

## PDF_PHYSICAL_PAGE_INDEX=314 | PRINTED_PAGE=298

对HL中。8080中的乘法子程序如下：

注意，上述子程序的第一行有一个标志Multiply。当然，实际上这个标志对应着子程序在存储器中的起始地址。该子程序在开始处使用了两个PUSH指令，这是因为通常在子程序的起始处要保存程序用到的寄存器。

保存寄存器后，子程序下面要做的是把寄存器H和L置 $0 _ { \circ }$ 尽管可以使用MVI（转移立即数）指令代替SUB指令来实现该操作，但这样会用到4个字节而不是2个字节的指令。子程序执行成功后，运算结果会保存到寄存器对HL中。

接下来子程序把寄存器B中的数（即乘数）转移到累加器A，并判断该数是否为 $\mathbf { 0 } _ { \circ }$ 如果为0，则乘法子程序结束，因为乘数为 $0 _ { \circ }$ 由于寄存器H和L已经为0，所以子程序可以使用 JZ（JumpIf Zero）指令跳转到程序最后的两条 POP指令。

如果乘数不是0，子程序会把寄存器B置为 $\mathbf { 0 } _ { \circ }$ 现在寄存器对 BC存放的是16 位的被乘数，而累加器中存放的是乘数。接下来DAD指令会把BC（被乘数）加到HL（运算结果）中。A中的乘数减1，如果结果不为0，则执行JNZ（非零跳转）指令，该指令会使BC再次加到HL。这个循环会继续执行，直到循环的次数等于乘数为止（当然也可以利用8080的移位指令编写一个更有效率的乘法子程序)。

编码——隐匿在计算机软硬件背后的语言

298

## PDF_PHYSICAL_PAGE_INDEX=315 | PRINTED_PAGE=299

在程序中使用如下指令来调用这个乘法子程序，例如，把25h和12h相乘：

CALL指令把PC的值保存到堆栈中，被保存的这个值是CALL指令的下一条指令的地址，然后 CALL指令将使程序跳转到标志为Multiply的指令，即子程序的起始处。当子程序得到计算结果后，执行RET（返回）指令，该指令使保存在堆栈的PC的值弹出，并重新设置到PC，之后程序将继续执行CALL指令后面的指令。

8080指令集包括条件 CALL指令和条件Return指令，但它们使用的频率比条件跳转指令小得多。下面的表格完整地列出了这些指令。

正如你大概所了解的，存储器并不是连接在微处理器上的唯一设备。一个完整的计算机系统通常需要输入/输出设备（I/O）以实现人机交互。输入/输出设备通常包括键盘和显示器等。

微处理器是如何与外围设备（peripheral，除存储器外，与微处理器连接的所有设备都可以称为外围设备）互相通信的呢？外围设备配备了与存储器类似的接口，微处理器通过与某种外围设备对应的特定地址（即接口）对其进行读写操作。在某些微处理器中，外围设备实际上占用了一些通常用来寻址存储器的地址，这种结构称作内存映像I/O(memory-mappedI/O)。但在8080中，除了常规的65536个地址外，另外增加了256个地址专门用来访问输入/输出设备，它们被称作I/O端口（I/O ports）。I/O地址信号标记为$\mathbf { A } _ { 0 } \sim \mathbf { A } _ { 7 }$ ，但I/O的访问方式与存储器的访问方式不同，两者的区分由8228系统控制芯片的锁存信号来标识。

19两种典型的微处理器

299

## PDF_PHYSICAL_PAGE_INDEX=316 | PRINTED_PAGE=300

OUT(输出)指令把累加器中的内容写入到紧跟该指令后的字节所寻址的端口(port)。IN（输入）指令把一个字节从端口读入到累加器。它们的格式如下所示。

外围设备有时候需要获得处理器的注意。例如，当你按下键盘的某个键时，处理器应该马上注意到这个事件。这个过程由一个称为中断（interrupt）的机制实现，这是一个由外围设备产生的信号，连接至8080的INT输入端。

但是，当8080复位后，就不再响应中断。程序必须执行EI（Enable Interrupt）指令来允许中断，然后执行DI（Disable Interrupts）禁止中断。这两条指令如下所示。

8080的INTE输出信号用来指明何时允许中断。当外围设备需要中断微处理器的当前工作时，它需要把8080的INT输入信号置为1。8080通过从存储器中取出指令来响应该中断，同时控制信号指明有中断发生。外围设备通常提供下列指令来响应8080微处理器。

上面列出的这些指令都称作Restart（重新启动）指令，在其执行的过程中也会把当前 PC 中的数据保存到堆栈，这一点与 CALL 指令类似。但 Restart 指令在保存 PC 数据之后会立刻跳转到特定的地址，而且是根据参数的不同将跳转到不同的地址：比如RST0将跳转到地址 0000h处，RST1将跳转到地址 00008h处，依此类推，最后的RST7将跳转到地址0038h处。这些地址存放的代码都是用来处理中断的。例如，由键盘引起的中断将执行RST4指令，程序将跳转到地址0020h处，该地址存放的代码将负责从键盘读入数据（完整的过程将在第21章讲述）。

编码隐匿在计算机软硬件背后的语言

300

## PDF_PHYSICAL_PAGE_INDEX=317 | PRINTED_PAGE=301

目前为止，我们已经介绍了243个操作码。在前255个数中，有12个没有作为操作码使用，它们是：08h，10h，18h，20h，28h，30h，38h，CBh，D9h，DDh，EDh和FDh。下面还需要讲到一个操作码。

NOP代表（即声明）no op（no operation，无操作）。NOP指令使处理器什么操作也不执行。这样做有什么好处呢？填空，即保持处理器的运行状态而不做任何事情。8080可以执行一批NOP指令而不会引起任何错误事件的发生。

本章不准备详细介绍Motorola 6800微处理器，因为在构造和功能方面它与8080非常相似。下面是6800的40个管脚的功能描述图。

$$
\begin{array} { r l r } { \nabla _ { \mathbf { S } _ { 0 } } \cdot \mathbf { u } } & { = } & { \nabla _ { \mathbf { S } _ { 0 } } \cdot \mathbf { u } _ { 0 } } \\ { \nabla _ { \mathbf { S } _ { 1 } } \cdot \mathbf { u } _ { 0 } - \left[ \begin{array} { l l l l } { 1 } & { - \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } \\ { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } \\ { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } \end{array} \right] } \\ { \nabla _ { \mathbf { S } _ { 0 } } \cdot \mathbf { u } _ { 0 } - \left[ \begin{array} { l l l l } { 1 } & { - \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } \\ { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } \\ { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } & { \frac { 1 } { 2 } } \end{array} \right] } & { = } & { \nabla _ { \mathbf { S } _ { 0 } } \cdot \mathbf { u } _ { 0 } } \\ { \nabla _ { \mathbf { S } _ { 0 } } \cdot \mathbf { u } _ { 0 } - \frac { \rho _ { 0 } } { 2 } } & { = } & { \nabla _ { \mathbf { S } _ { 0 } } \cdot \mathbf { u } _ { 0 } } \\ { \nabla _ { \mathbf { S } _ { 1 } } \cdot \mathbf { u } _ { 0 } - \frac { 1 } { 2 } } & { = } &  \nabla _  \end{array}
$$

在上图中， $\mathbf { V _ { S S } }$ 表示接地， $\mathbf { V } _ { \mathbf { C C } }$ 代表5V的电源。同8080一样，6800也有16个地址输出信号端和8个数据信号端，其中数据信号端既可以用于输入信号也可以用于输出信号。它还有一个RESET信号端和一个R/W（read/write，读/写）信号。IRQ信号代表中断请求。与8080相比，6800的时钟信号较为简单，6800没有设计独立的I/O端口，所有的输入输出设备的地址都是存储器地址空间的一部分。

19 两种典型的微处理器

301

## PDF_PHYSICAL_PAGE_INDEX=318 | PRINTED_PAGE=302

6800有一个16位的程序计数器PC、一个16位的堆栈指针SP、一个8位的状态寄存器（用来保存标志位），以及两个8位的累加器A、B。A和B都可以用做累加器（而不是把B作为普通的寄存器)，因为A和B的功能完全相同，任何用A做的工作都可以用B实现。与8080不同，6800没有设置其他的8位寄存器。

6800设置了一个16位的索引寄存器（index register)，它可以用来保存16位的地址，其功能与8080的HL寄存器对相似。对于6800的大部分指令来说，它们的地址都可以由索引寄存器与紧跟在操作码后的字节相加得到。

尽管6800实现的操作与8080大致相同加载、保存、加法、减法、移位、跳转、调用等，但对应的操作码和助记符是完全不同的。例如，下面列出了6800的转移(Branch)指令集。

编码—隐匿在计算机软硬件背后的语言

302

## PDF_PHYSICAL_PAGE_INDEX=319 | PRINTED_PAGE=303

与8080不同，6800没有设置奇偶标志位，而是设置了一个溢出标志位（Overflowflag)。上面的转移指令中有一些依赖于标志位的组合(combinations of flags)。

当然，8080和6800的指令集是不同的，虽然这两款芯片于同一年发布，但它们是由属于不同公司的两组不同的工程师设计的。这就造成了它们之间的不兼容，因此它们不能执行对方的机器码，为一种芯片编写的汇编语言程序也不能在另一种芯片上执行。如何编写能在不同类型处理器上执行的计算机程序是第24章的主题。

8080和6800的另一个有趣的区别是：在两个处理器中，LDA指令都从存储器的特定地址将数据加载到累加器。例如，在8080中，下面的字节序列：

将把存储器地址347Bh处的字节加载到累加器。现在对比一下6800的LDA指令，它使用 6800 扩展寻址模式（6800 extended addressing mode):

上面的这组字节序列将把存储器7B34h地址处的字节加载到累加器。

两者的区别是很微妙的。当然，你可能已经注意到了操作码的不同：8080的操作码是3Ah，而6800的操作码是B6h。但是两种微处理器对紧跟在操作码后的地址的处理方式是不同的，8080假设低字节在前，高字节在后；而6800假设高字节在前，低字节在后。

Intel和Motorola的微处理器在保存多字节数据问题上的根本区别从未得到解决。直到今天，英特尔的微处理器在保存多字节数据时，仍然把最低有效字节放在最前面（也就是说，在最低地址处)，而Motorola的微处理器在保存多字节数据时，仍然把最高有效字节放在最前面。

这两种不同的方式分别称为little-endian（Intel方式）和 big-endian（Motorola方式）。

19两种典型的微处理器

303

下一段：[继续读取](https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020.md?start=320&limit=10)
