# 指令系统
## 基本概念
## 指令格式
## 寻址方式
## 数据的对齐和大小端存放方式
## CISC和RISC
## 高级程序语言与机器代码之间的对应
- 编译器、汇编器和链接器
- 选择结构语句
- 循环结构语句
- 过程调用对应的机器级表示
指令系统
1 - 指令格式和寻址方式
计算机程序的执行过程
计算机程序的生命周期通常包括编译(compile)、汇编(assemble)、链接(linking)和执行(execute)等阶段。这些阶段是构建和运行程序的重要步骤。以下是这些阶段的简要说明:
- 编译(Compile):
- 编译是将高级编程语言(如 C、C++、Java 等)源代码翻译为目标机器的汇编语言或机器代码的过程。
- 编译器(如 GCC、Clang、Visual C++)负责将源代码转化为目标代码,并生成一个中间表示,如汇编语言或机器码。
- 这个阶段的目标是检查源代码中的语法错误和逻辑错误,并生成可执行程序的中间文件。
- 汇编(Assemble):
- 汇编是将汇编语言源代码(通常是由编译器生成的中间表示)转化为机器码的过程。
- 汇编器(如 NASM、MASM)负责将汇编语言代码转化为可执行程序所需的机器码。
- 这个阶段的目标是将源代码翻译为可执行代码,并生成一个目标文件。
- 链接(Linking):
- 链接是将程序的不同部分(如多个源代码文件、库文件等)组合在一起,以创建最终的可执行程序的过程。
- 链接器(如 ld、linker)负责解析程序中的符号引用,将它们与符号定义(如函数、变量)关联起来,以创建一个完整的可执行文件。
- 这个阶段的目标是解决符号引用,创建可执行程序,并将各种模块整合到一个单独的可执行文件中。
- 执行(Execute):
- 执行是将最终生成的可执行程序加载到内存中,并由计算机的中央处理单元(CPU)执行的过程。
- 操作系统负责加载可执行程序,并将控制权转交给程序的起始点。
- 可执行程序的指令将由 CPU 执行,从而实现程序的功能。
指令的格式
指令中主要包含两个部分:操作码(opcode)以及 地址(address)。
这里地址是一个通用含义,指的是操作的对象,可以是一个内存地址,也可以是 CPU 中的一个寄存器。
指令类型
根据指令中的地址个数,可以将指令划分为以下类型。
指令格式 | 含义 |
---|---|
零地址指令 | $OP$ |
一地址指令 | $OP(A_1) \rightarrow A_1$ |
二地址指令 | $(A_1)OP(A_2) \rightarrow A_1$ |
三地址指令 | $(A_1)OP(A_2) \rightarrow A_3$ |
指令根据其操作码(opcode)的不同可以分为以下类别:
- 数据传输指令
MOV
:将数据从一个位置传输到另一个位置,可以是寄存器到寄存器、内存到寄存器、寄存器到内存等。PUSH
:将数据(通常是寄存器中的值)推入堆栈。POP
:从堆栈中弹出数据并存储到寄存器中。
- 算术和逻辑运算指令
ADD
、SUB
、MUL
、DIV
:执行算术运算,如加法、减法、乘法和除法。AND
、OR
、XOR
、NOT
:执行逻辑运算,如按位与、按位或、按位异或和按位取反。INC
、DEC
:递增和递减操作数的值。CMP
:用于比较两个值,并根据结果设置标志寄存器的状态。
- 控制转移指令
JMP
:用于无条件跳转到指定的目标地址。Jxx
:条件跳转指令,根据特定的条件(如零标志、进位标志等)来决定是否跳转。CALL
:调用子程序或函数。RET
:从子程序返回。
- 输入/输出指令
IN
:从外部设备或端口读取数据。OUT
:向外部设备或端口发送数据。
- 字符串操作指令(String Instructions):
MOVS
、LODS
、STOS
、CMPS
:用于在内存中执行字符串操作,如移动、加载、存储、比较。
- 陷阱指令(Trap Instructions):
INT
:用于引发中断,通常用于与操作系统进行通信。
- 协处理器指令(Coprocessor Instructions):
CLI
、STI
:用于清除和设置 CPU 的中断标志,通常只能在内核模式下执行。
寻址方式
计算机中的寻址方式(Addressing Modes)是指在指令中如何指定操作数的位置或地址。
寻址方式 | 描述 | 示例 |
---|---|---|
立即寻址 | 操作数直接包含在指令中。 | MOV R1, #5 (将5加载到R1寄存器) |
寄存器寻址 | 操作数在寄存器中。 | ADD R1, R2 (R2加到R1中) |
直接寻址 | 操作数的内存地址直接包含在指令中。 | MOV R1, [1000] (从内存地址1000取数据) |
间接寻址 | 操作数的地址由指令指定的内存地址提供,指令通过这个地址访问操作数。 | MOV R1, [R2] (R2中存储的是操作数的地址) |
寄存器间接寻址 | 操作数的地址存储在寄存器中,指令通过寄存器访问内存中的数据。 | MOV R1, [R2] (R2寄存器中是内存地址) |
基址寻址 | 使用基址寄存器和偏移量计算操作数的实际地址。 | MOV R1, [R2 + 4] (基址R2加偏移4) |
变址寻址 | 通过基址寄存器和索引寄存器的和来确定操作数地址,常用于数组操作。 | MOV R1, [R2 + R3] (R2与R3相加) |
相对寻址 | 操作数地址通过程序计数器(PC)当前值加上指令中的偏移量计算,常用于跳转指令。 | JMP LABEL (跳转到相对地址) |
堆栈寻址 | 通过堆栈顶指针(SP)来访问操作数,常用于函数调用和返回。 | PUSH R1 (将R1压入堆栈) |
2 - 数据对齐方式
数据对齐
数据对齐是指数据在内存中的存放方式,它要求数据的起始地址必须是某个数(通常是4或8)的整数倍,这个数被称为对齐因子(Alignment Factor)或对齐边界(Alignment Boundary)。数据对齐的目的是为了提高内存访问的效率,因为许多计算机系统都是按照数据的对齐边界来设计内存访问硬件的。
不对齐的数据访问可能会导致性能下降,因为处理器可能需要额外的内存访问来获取不完整的数据。在一些严格要求数据对齐的架构中,不对齐的数据访问甚至会导致硬件异常。
例如,假设一个整数(int)占用4个字节,那么在内存中的起始地址就应该是4的倍数,如0x1004、0x1008等。
大小端
大小端(Endianness)是指多字节数据在内存中的字节序,也就是字节的排列顺序。主要有两种存放方式:
- 大端模式(Big-Endian): 数据内部的高位字节存放在低位地址,低位字节存放在高位地址。也就是说,一个整数的第一个字节(最高有效字节)将存放在起始地址处。
- 小端模式(Little-Endian): 数据内部的低位字节存放在低位地址,高位字节存放在高位地址。也就是说,一个整数的最后一个字节(最低有效字节)将存放在起始地址处。
举一个例子,假如定义数组 long a[2] = {0x76543210, 0xFEDCBA98}
,long
类型的大小为8字节,数组 a
在内存中的起始地址为 0x1000
,则数组中两个元素在内存中的字节排列如下图所示:
大小端的选择通常是由计算机的CPU架构决定的,不同的架构有不同的字节序要求。例如,Intel x86和x86-64架构是小端,而网络协议通常是大端,因为大端的格式在字节流中的表示更加直观。
3 - CISC和RISC
CISC(Complex Instruction Set Computer)和 RISC(Reduced Instruction Set Computer)是两种不同的计算机体系结构设计哲学,它们在指令集架构和执行方式上有显著的差异。以下是它们的主要特点和区别:
CISC
- 指令集复杂:CISC计算机的指令集非常丰富,包含大量复杂的指令,其中一条指令可以执行多种操作,包括内存访问、算术运算、逻辑运算等。
- 多寻址模式:CISC指令通常支持多种寻址模式,允许直接访问内存,因此可以在一条指令中执行复杂的操作。
- 微程序控制:CISC计算机通常使用微程序控制单元,指令解码和执行过程相对复杂。
- 复杂硬件:CISC处理器通常包括大量的硬件单元,用于支持复杂的指令集,这使得CISC芯片相对较大。
RISC
- 指令集精简:RISC计算机的指令集更加精简,通常包含较少、更简单的指令。每条指令只执行一种操作。
- 固定寻址模式:RISC指令通常只支持一种或者很少种寻址模式,鼓励将数据加载到寄存器中后再执行操作。
- 硬布线控制:RISC计算机使用硬布线控制单元,指令解码和执行过程较为简单。
- 精简硬件:RISC处理器通常采用更精简的硬件,以提高性能和降低成本。
主要区别和优点:
- CISC体系结构通过提供复杂的指令来减少编程工作,但它们可能会导致较慢的执行速度和复杂的硬件设计。
- RISC体系结构通过简化指令集和加速执行来提高性能,但需要更多的指令以执行相同的任务。
4 - 高级语言和机器码
编译器、汇编器、链接器
编译器 (Compiler)
编译器是一个软件程序,它将用高级程序设计语言(如C、C++、Java)编写的源代码转换为低级语言(通常是汇编语言)。编译器主要进行以下工作:
- 词法分析 (Lexical Analysis):将源代码分解成一系列的记号(tokens)。
- 语法分析 (Syntax Analysis):将记号组织成语法树,检查代码的语法结构。
- 语义分析 (Semantic Analysis):检查代码的语义正确性,比如类型检查。
- 优化 (Optimization):对代码进行优化,提高效率,减少资源消耗。
- 代码生成 (Code Generation):将优化后的代码转换为目标机器的汇编语言。
例如,GCC(GNU Compiler Collection)和MSVC(Microsoft Visual C++)都是流行的编译器。
汇编器 (Assembler)
汇编器是将汇编语言转换为机器语言的程序。因为汇编语言中的指令基本上是机器指令的直接映射(只是用符号代替了二进制代码),所以汇编器主要工作是:
- 解析指令:识别汇编代码中的指令和数据定义。
- 符号解析:解析标签和符号,将它们转换成地址或者数值。
- 生成机器代码:将汇编指令和操作数转换为对应的机器码。
链接器 (Linker)
链接器的作用是将由编译器生成的一个或多个目标代码文件(通常是汇编器生成的机器代码)合并为一个单一的可执行文件。在这个过程中,链接器:
- 解析外部引用:将不同代码模块之间相互引用的部分关联起来。
- 地址和存储分配:确定代码和数据在内存中的位置。
- 符号绑定:将程序中的符号(如函数和变量名)绑定到它们对应的内存地址。
- 重定位:调整代码和数据的引用,使它们指向正确的地址。
- 库链接:将程序使用的库中的代码合并到最终的可执行文件中。
链接器可以是静态链接器,它在程序开始执行之前完成所有的链接工作;也可以是动态链接器(运行时链接器),它在程序执行时进行链接。
选择结构语句
if (a > b) {
max = a;
} else {
max = b;
}
; 假设a, b的值分别存放在寄存器eax和ebx中
cmp eax, ebx ; 比较a和b
jle else_label ; 如果a <= b, 跳转到else_label
mov max, eax ; a > b, max = a
jmp endif_label ; 跳转到endif_label
else_label:
mov max, ebx ; max = b
endif_label:
循环结构语句
while (count < 10) {
count++;
}
; 假设count的值存放在寄存器ecx中
loop_start_label:
cmp ecx, 10 ; 比较count和10
jge loop_end_label ; 如果count >= 10, 跳出循环
inc ecx ; count增加
jmp loop_start_label ; 无条件跳回循环开始
loop_end_label:
过程调用
在汇编中调用函数包含如下步骤:
- 参数传递:在函数调用之前,将参数值放入寄存器或栈中。
- 调用指令:使用call指令将控制转移到函数的代码。
过程调用内部包含如下步骤:
- 堆栈帧设置:在函数的开始处,设置局部变量所需的堆栈帧。
- 执行函数体:执行函数中的代码。
- 返回值处理:将函数的返回值放在一个特定的位置,通常是一个寄存器。
- 堆栈帧恢复和返回:恢复调用者的堆栈帧并执行ret指令,这会跳转回调用点。
以下举一个实际的例子(了解即可):
int add(int x, int y) {
return x + y;
}
void exampleFunction(int a, int b) {
int sum = add(a, b);
int localVariable = sum * 2;
// ... 一些使用localVariable的代码 ...
}
// 调用exampleFunction
exampleFunction(10, 20)
; 假定 'a' 和 'b' 作为参数通过堆栈传递
.globl _add
_add:
push ebp ; 保存旧的基指针
mov ebp, esp ; 设置新的基指针
mov eax, [ebp+8] ; 将参数x移到eax,参数x在ebp+8的位置
add eax, [ebp+12] ; 将参数y加到eax中,参数y在ebp+12的位置
pop ebp ; 恢复旧的基指针
ret ; 返回,返回值在eax中
.globl _exampleFunction
_exampleFunction:
push ebp ; 保存旧的基指针
mov ebp, esp ; 设置新的基指针
sub esp, 8 ; 在堆栈上分配8字节空间给局部变量
; 准备参数并调用 'add'
push dword [ebp+12] ; 将参数b压栈
push dword [ebp+8] ; 将参数a压栈
call _add ; 调用 'add'
add esp, 8 ; 清理传递给 'add' 的参数
; 将返回值 'sum' 存储到局部变量
mov [ebp-4], eax ; 将eax的值('add' 的返回值)存储在 'sum' 的位置
; 创建另一个局部变量 'localVariable' 并计算 'sum * 2'
mov eax, [ebp-4] ; 将 'sum' 移到eax
shl eax, 1 ; 将eax左移1位,相当于乘以2
mov [ebp-8], eax ; 将结果存储到 'localVariable'
; ... 更多使用 'localVariable' 的代码 ...
; 函数完成,清理堆栈,并恢复ebp
mov esp, ebp ; 重置堆栈指针
pop ebp ; 恢复基指针
ret ; 返回
; 函数调用的汇编表示
; 假设要传递给 exampleFunction 的参数为 10 和 20
; 先将参数推入堆栈,注意在32位架构中调用约定通常是从右到左传递参数
push 20 ; 将第二个参数 b 压入堆栈
push 10 ; 将第一个参数 a 压入堆栈
call _exampleFunction ; 调用 exampleFunction 函数
add esp, 8 ; 清理堆栈,移除参数(每个参数4字节,总共8字节)
; 在这里执行之后的代码
; ...