这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

指令系统

本章也是重点,常常在大题中与CPU放在一起考察。需要熟练掌握指令的格式以及寻址方式,并且能够根据题目灵活应变。
# 指令系统

## 基本概念

## 指令格式

## 寻址方式

## 数据的对齐和大小端存放方式

## CISC和RISC

## 高级程序语言与机器代码之间的对应

- 编译器、汇编器和链接器
- 选择结构语句
- 循环结构语句
- 过程调用对应的机器级表示

1 - 指令格式和寻址方式

指令格式是计算机组成原理中的重点考察内容,需熟练掌握,熟练张常常在大题中与 CPU 的知识交叉考察。

指令的格式

指令中主要包含两个部分:操作码(opcode)以及 地址(address)。

这里地址是一个通用含义,指的是操作的对象,可以是一个内存地址,也可以是 CPU 中的一个寄存器。

指令类型

根据操作码分类

指令根据其操作码(opcode)的不同可以分为以下类别:

  1. 数据传输指令
    • MOV:将数据从一个位置传输到另一个位置,可以是寄存器到寄存器、内存到寄存器、寄存器到内存等。
    • PUSH:将数据(通常是寄存器中的值)推入堆栈。
    • POP:从堆栈中弹出数据并存储到寄存器中。
  2. 算术和逻辑运算指令
    • ADDSUBMULDIV:执行算术运算,如加法、减法、乘法和除法。
    • ANDORXORNOT:执行逻辑运算,如按位与、按位或、按位异或和按位取反。
    • INCDEC:递增和递减操作数的值。
    • CMP:用于比较两个值,并根据结果设置标志寄存器的状态。
  3. 控制转移指令
    • JMP:用于无条件跳转到指定的目标地址。
    • Jxx:条件跳转指令,根据特定的条件(如零标志、进位标志等)来决定是否跳转。
    • CALL:调用子程序或函数。
    • RET:从子程序返回。
  4. 输入/输出指令
    • IN:从外部设备或端口读取数据。
    • OUT:向外部设备或端口发送数据。
  5. 字符串操作指令(String Instructions):
    • MOVSLODSSTOSCMPS:用于在内存中执行字符串操作,如移动、加载、存储、比较。
  6. 陷阱指令(Trap Instructions):
    • INT:用于引发中断,通常用于与操作系统进行通信。
  7. 协处理器指令(Coprocessor Instructions):
    • CLISTI:用于清除和设置 CPU 的中断标志,通常只能在内核模式下执行。

根据地址个数分类

根据指令中的地址个数,可以将指令划分为以下类型。

OP
OP
A1
OP
A1
A2
OP
A3
A1
A2
零地址
一地址
二地址
三地址
指令格式指令格式含义
零地址指令op$op$
一地址指令op, A1$op(A_1) \rightarrow A_1$
二地址指令op, A1, A2$(A_1)op(A_2) \rightarrow A_1$
三地址指令op, A3, A1, A2$(A_1)op(A_2) \rightarrow A_3$

“N 地址指令” 中的 “地址” 一词可以是立即数或一个内存地址,也可以是寄存器。

定长和变长指令

变长指令集
定长指令集

定长指令集中所有指令长度相同,变长指令集中会包含不同长度的指令。

使用定长指令集的典型 cpu 架构是 arm(RISC),优点是解码简单、高效,但可能导致指令浪费空间。

使用变长指令集的典型 cpu 架构是 x86(CISC),优点是可以更加紧凑地编码复杂操作,但解码更加复杂。

寻址方式

计算机中的寻址方式(Addressing Modes)是指在指令中如何指定操作数的位置或地址。

立即数寻址

操作数作为指令的一部分而直接写在指令中,这种操作数称为立即数,这种寻址方式也就称为立即数寻址方式。

• • • • • 
OP
76H
45H
• • • • • 
MOV AX, 4576H
76H
45H
立即数寻址
AX
指令存储在 text 段在内存中

立即数寻址方式通常用于对通用寄存器或内存单元赋初值,上图是指令 MOV AX, 4567H 存储形式和执行示意图,直接将 指令中的数据 4567H 存储到寄存器中。

直接寻址

MOV R1, 1000
3456H
R1
56H
高地址
低地址
1000H
0
FFFFH
34H
从相应内存地址中读取数据
存储到 R1 中
内存
10
OP
00
译码执行

间接寻址

MOV R1, [R2]
7890H
1234H
R1
R2
90H
高地址
低地址
1234H
0
FFFFH
78H
从相应内存地址中读取数据
存储到 R1 中
内存
R1
OP
R2
译码执行

基址寻址

8086 的存储器被分成若干段,其中会专门设置一个基址存储器用于存储段的起始地址,段内的位移量由指令给出。操作数的地址由基址寄存器的内容和指令的地址码 A 相加得到,这种情况下地址码 A 常被成为位移量。

OP
Rb
A
基址寄存器
(Rb) + A = E
操作数
E:
指令
基址
(寻址标志)
逻辑地址
加法器
有效地址
地址
主存

变址寻址

指令地址码给出地址 A 和指定的变址寄存器 R,将 A 和 R 的内容相加就是操作数的地址。常利用变址操作与循环执行程序的方法对数组进行运算。

OP
......
基准地址 A
变址值
操作数
E:
指令
主存
ALU
有效地址 EA
变址寄存器 Rx

注意

基址和变址寻址的区别?

  • 基址寻址主要用于为程序或数据分配存储空间,故基址寄存器的内容通常由操作系统或管理程序确定,在程序运行过程中,值是不可变的,而指令字中的地址码是可变的。
  • 变址寻址中,变址寄存器的内容是用户自己设定的,在程序运行过程中是可变的,而指令字中的地址码是不可变的。编制寻址主要用于处理数组等问题,并且特别适合编制循环程序。

相对寻址

OP
......
A
PC
指令
E:
指令
主存
ALU
有效地址 EA
.text

堆栈寻址

根据 函数调用时的内存结构,函数栈可以存储函数中的变量。堆栈寻址就是在函数的栈帧中访问通过 PUSH 指令存储的数据。

caller BP
BP
SP
caller BP
BP
SP
PUSH A
A
caller BP
BP
SP
PUSH B
A
B
caller BP
BP
SP
POP
A
High Address
Low Address

寻址方式对比

寻址方式描述示例
立即寻址操作数直接包含在指令中。MOV R1, #5 (将 5 加载到 R1 寄存器)
寄存器寻址操作数在寄存器中。ADD R1, R2 (R2 加到 R1 中)
直接寻址操作数的内存地址直接包含在指令中。MOV R1, [1000] (从内存地址 1000 取数据)
间接寻址操作数的地址存储在寄存器中,指令通过寄存器访问内存中的数据。MOV R1, [R2] (R2 寄存器中是内存地址)
基址寻址使用基址寄存器和偏移量计算操作数的实际地址。MOV R1, [R2 + 4] (基址 R2 加偏移 4)
变址寻址通过基址寄存器和索引寄存器的和来确定操作数地址,常用于数组操作。MOV R1, [R2 + R3] (R2 与 R3 相加)
相对寻址操作数地址通过程序计数器(PC)当前值加上指令中的偏移量计算,常用于跳转指令。JMP LABEL (跳转到相对地址)
堆栈寻址通过堆栈顶指针(SP)来访问操作数,常用于函数调用和返回。PUSH R1 (将 R1 压入堆栈)

2 - 数据对齐方式

熟练掌握数据对齐和大小端,可能会在选择题考察,也会在大题中间接考察。

数据对齐

数据对齐(Data Alignment)是指数据在内存中的存放方式,它要求数据的起始地址必须是某个数(通常是1、2、4、8)的整数倍,这个数被称为对齐因子(Alignment Factor)。数据对齐的目的是为了提高内存访问的效率,因为许多计算机系统都是按照数据的对齐边界来设计内存访问硬件的。

不对齐的数据访问可能会导致性能下降,因为处理器可能需要额外的内存访问来获取不完整的数据。在一些严格要求数据对齐的架构中,不对齐的数据访问甚至会导致硬件异常。


在 C11 中,我们可以通过 _Alignof 来查看不同数据类型的对齐因子:

#include <stdint.h>
#include <stdio.h>

#define EVAL_PRINT(expr) printf("%-20s = %u\n", #expr, (uint8_t)(expr));

int main(void) {
    EVAL_PRINT(_Alignof(char));
    EVAL_PRINT(_Alignof(uint8_t));
    EVAL_PRINT(_Alignof(uint16_t));
    EVAL_PRINT(_Alignof(uint32_t));
    EVAL_PRINT(_Alignof(int));
    EVAL_PRINT(_Alignof(uint64_t));
    EVAL_PRINT(_Alignof(void*));
    EVAL_PRINT(_Alignof(size_t));
    return 0;
}

以上程序运行的结果如下所示,以此我们可以判断每个类型的 aglinment 大小。

$ gcc alignof.c && ./a.out
_Alignof(char)       = 1
_Alignof(uint8_t)    = 1
_Alignof(uint16_t)   = 2
_Alignof(uint32_t)   = 4
_Alignof(int)        = 4
_Alignof(uint64_t)   = 8
_Alignof(void*)      = 8
_Alignof(size_t)     = 8

以下图中的结构体定义为例,假设我们定义一个变量,变量的类型长度为 K 个字节,那么这个变量在内存中的地址 addr 必须是 K 的整数倍,即 addr % K == 0。

a
b
b
c
c
c
c
d
struct foo {
char a;
uint16_t b;
char d;
};
int32_t c;

上图中变量 b 和 a 之间增加了 1 个字节的 padding,变量 d 的末尾也增加了 3 个字节 padding,以保证下一个数据的开始是 4 的整数倍。

大小端

大小端(Endianness)是指多字节数据在内存中的字节序,也就是字节的排列顺序。主要有两种存放方式:

  • 大端模式(Big-Endian): 数据内部的高位字节存放在低位地址,低位字节存放在高位地址。也就是说,一个整数的第一个字节(最高有效字节)将存放在起始地址处。
  • 小端模式(Little-Endian): 数据内部的低位字节存放在低位地址,高位字节存放在高位地址。也就是说,一个整数的最后一个字节(最低有效字节)将存放在起始地址处。

举一个例子,假如定义数组 long a[2] = {0x76543210, 0xFEDCBA98}long 类型的大小为8字节,数组 a 在内存中的起始地址为 0x1000,则数组中两个元素在内存中的字节排列如下图所示:

0x10
0x32
0x54
0x65
0x00
0x00
0x00
0x00
0x00
0x00
0x00
0x00
0x76
0x54
0x32
0x10
0x89
0xBA
0xDC
0xFE
0x00
0x00
0x00
0x00
0x00
0x00
0x00
0x00
0xFE
0xDC
0xBA
0x89
0x1000
0x1000
0x1008
0x1008
Little Endian
Big Endian

大小端的选择通常是由计算机的CPU架构决定的,不同的架构有不同的字节序要求。例如,Intel x86和x86-64架构是小端,而网络协议通常是大端,因为大端的格式在字节流中的表示更加直观。

3 - 指令集种类

本节主要了解 CISC 和 RISC 的概念,可能在选择题中考察。后面不同位数计算机指令的差异 以及 AT&T、Intel 指令集可能在大题中间接考察。

复杂和精简指令集

CISC(Complex Instruction Set Computer)和 RISC(Reduced Instruction Set Computer)是两种不同的计算机体系结构设计哲学,它们在指令集架构和执行方式上有显著的差异。

csic 和 risc 处理器

CISC

  1. 指令集复杂:CISC 指令集包含大量复杂的指令,其中一条指令可以执行多种操作,包括内存访问、算术运算、逻辑运算等。
  2. 指令不定长:CISC 支持多种长度的指令。
  3. 多寻址模式:CISC 指令通常支持多种寻址模式,允许直接访问内存,因此可以在一条指令中执行复杂的操作。
  4. 微程序控制:CISC 计算机通常使用微程序控制单元,指令解码和执行过程相对复杂。
  5. 复杂硬件:CISC 处理器通常包括大量的硬件单元,用于支持复杂的指令集,这使得 CISC 芯片相对较大。

CISC 的典型芯片就是 x86 系列,如 Intel 的 Core i 系列处理器和 AMD 的 Ryzen 系列处理器。

RISC

  1. 指令集精简:RISC 计算机的指令集更加精简,通常包含较少、更简单的指令。每条指令只执行一种操作。
  2. 指令定长:RISC 指令集中所有指令长度相同。
  3. 固定寻址模式:RISC 指令通常只支持一种或者很少种寻址模式,鼓励将数据加载到寄存器中后再执行操作。
  4. 硬布线控制:RISC 计算机使用硬布线控制单元,指令解码和执行过程较为简单。
  5. 精简硬件:RISC 处理器通常采用更精简的硬件,以提高性能和降低成本。

ax
bx
cx
dx
si
di
sp
bp
cs
ds
es
fs
gs
ss
x0
x1
x2
x3
x8
x9
x10
x11
x16
x17
x18
x19
x24
x25
x26
x27
x4
x5
x6
x7
x12
x13
x14
x15
x20
x21
x22
x23
x28
x29
x30
x31
ADD  4(x2), x3
LW  x4, 4(x2)
ADD  x3, x4, x3
SW  x3, 4(x2)
mem[x2+4] ← mem[x2+4] + x3
CSIC
RISC
复杂指令集
复杂指令集
x86 16位 cpu 8086
的寄存器
寄存器数量较少
寄存器数量较多
寄存器数量
指令的内存访问模式
内存访问模式复杂,可以在
一条指令中对内存读和写
x4 ← mem[x2+4]
x3 ← x4 + x3      
x3 → mem[x2+4]
内存访问模式简单,只能在一条指令中
对内存进行 load 或 store

上图体现了 CISC 和 RISC 的差别:RISC 寄存器数量比 CISC 更多,CISC 的访存指令比 RISC 更加复杂(CSIC 单条指令完成的工作 RISC 需要多条指令才能完成)。

程序示例

这一节举个实际的例子说明一下 CISC 和 RISC 上的汇编代码区别。

for (i = 0; i < 24; i++)
    for (j = 0; j < 24; j++)
        ...
        a[i][j] = 10;
        ...

对于上述的循环代码段,编译器将其编译为如下的 intel x86 汇编代码(CISC) 和 arm mips 汇编代码(RISC):

for (i = 0; i < 24; i++)
1  00401072  C7 45 F8 00 00 00 00      mov     dword ptr [ebp-8], 0   ; i = 0,初始化 i 变量
2  00401079  EB 09                     jmp     00401084h              ; 跳转到循环条件检查
3  0040107B  8B 55 F8                  mov     eax, [ebp-8]           ; 读取 i 的值到 eax
    ... ... ...
7  00401088  7D 32                     jge     004010BCh              ; 若 i >= 24,跳出循环

    for (j = 0; j < 64; j++)
8  0040108A  C7 45 FC 00 00 00 00      mov     dword ptr [ebp-4], 0   ; j = 0,初始化 j 变量
        ... ... ...
        a[i][j] = 10;
        ... ... ...
19 004010AE  C7 84 82 00 20 42 00 0A 00 00 00  
                                       mov     dword ptr [ecx+edx*4+00422000h], 0Ah  
                                                                      ; 存储 10 到 a[i][j],计算地址:
                                                                      ; ecx = i, edx = j
                                                                      ; a[i][j] = 10
20 ... ...
for (i = 0; i < 24; i++)
1  00401000  20020000      addi  $v0, $zero, 0       # i = 0
2  00401004  08004004      j     0x00401010          # 跳转到循环条件检查
3  00401008  8C430008      lw    $v1, 8($fp)         # 读取 i
    ... ... ...
7  00401010  1C600018      bge   $v1, 24, 0x00401040 # if (i >= 24) 跳出循环
    for (j = 0; j < 64; j++)
8  00401014  20040000      addi  $a0, $zero, 0       # j = 0
        ... ... ...
        a[i][j] = 10;
17 00401028  00031880      sll   $v1, $v1, 6         # i × 64
18 0040102C  00641820      add   $v1, $v1, $a0       # i × 64 + j
19 00401030  00031880      sll   $v1, $v1, 2         # (i × 64 + j) × 4
20 00401034  3C010042      lui   $at, 0x0042         # 加载基地址高 16 位
21 00401038  34222000      ori   $v0, $at, 0x2000    # 完整基地址 0x00422000
22 0040103C  00621020      add   $v0, $v1, $v0       # 计算目标地址
23 00401040  2405000A      li    $a1, 10             # 10 存入 $a1
24 00401044  AC450000      sw    $a1, 0($v0)         # a[i][j] = 10
25 ... ...

CISC 和 RISC 的简要对比如下所示:

特性CISCRISC
指令集复杂,指令数量多精简,指令数量少
指令长度不定长固定长度
寻址模式多种寻址模式较少寻址模式
控制方式微程序控制硬布线控制
硬件复杂度复杂精简
优点功能强大,一条指令可完成复杂操作性能高,功耗低
缺点硬件复杂,指令执行效率相对较低功能相对简单,复杂操作需要多条指令完成

不同位数 cpu 的指令

AH
AL
AX
(16 bits)
EAX
(32 bits)
RAX
(64 bits)

从 8086(16 位 intel 处理器)再到后来的 32 位以及 64 位的 CPU,差异主要体现在以下几个方面:

  1. 寄存器位数和命名不同:32 位寄存器有 E 前缀,64 位寄存器有 R 前缀。
  2. 指令集不同:32 位处理器相比 16 位处理器有着更加复杂的指令集,64 位处理器的指令集也比 32 位更加复杂。

下表中给出了不同位数寄存器的对比,注意每个寄存器的开头都与一个单次相对应,在最初 8086 的设计中这是代表一种语义。

寄存器16 位32 位64 位类型
AccumulatorAXEAXRAXGeneral
BaseBXEBXRBXGeneral
CounterCXECXRCXGeneral
DataDXEDXRDXGeneral
Source IndexSIESIRSIPointer
Destination IndexDIEDIRDIPointer
Base PointerBPEBPRBPPointer
Stack PointerSPESPRSPPointer
Instruction PointerIPEIPRIPPointer
Code SegmentCS--Segment
Data SegmentDS--Segment

AT&T 和 Intel 指令

在 x86 汇编指令集中,常有 AT&T 和 Intel 两种格式,两种格式有较大差异。在考试中主要考察的是 Intel 格式,但是 AT&T 也需稍作了解,在遇到指令时能辨认出即可。

Intel 格式


# 寄存器访问
mov     eax,1
mov     ebx,0ffh
# 内存访问
mov     eax,[ebx]
mov     eax,[ebx+3]

AT&T 格式


# 寄存器访问
movl    $1,%eax
movl    $0xff,%ebx
# 内存访问
movl    (%ebx),%eax
movl    3(%ebx),%eax

两种指令格式的不同主要在于 Intel 格式的 目的操作数在左,源操作数在右,而 AT&T 格式 目的操作数在右,源操作数在左。

两种指令格式的具体表格如下表所示:

特性Intel 格式AT&T 格式
操作数顺序目的,源源,目的
寄存器eax, ebx...%eax, %ebx...
常数10, 0x20...$10, $0x20...
内存寻址[]()

4 - 高级语言和机器码

掌握从高级语言到机器代码的翻译过程,也需了解C语言中不同语句与汇编代码的对应关系,可能在大题中出现,常与 cache 和 虚拟内存 放在一起考察。

编译过程

Preprocessor
Compiler
Assembler
Linker
Source Code
Executable Code
Expanded Code
Assembly Code
Object File

一个传统的 C 程序从源代码到可执行二进制程序的过程中,需要经历预处理(Preprocess)、编译(Compile)、汇编(Assemble)、链接(Link)四个步骤,这四个步骤分别由预处理器(Preprocessor)、编译器(Compiler)、汇编器(Assembler)、链接器(Linker)完成。

预处理

预处理(Preprocess)阶段负责对源代码(source code)进行文本的转换和处理,将其转化为扩展代码(expanded code)。具体而言,预处理阶段包含如下工作:

  • 头文件包含:将 #include 指令包含的头文件内容插入到源文件中。
  • 宏替换: 将源代码中定义的宏 #define 进行替换。
  • 删除注释: 将代码中的注释删除。

编译

编译(Compile)阶段将预处理后的源代码(extended code)翻译成汇编代码(assembly code)。编译阶段包含词法分析、语法分析、语义分析、中间代码优化和汇编代码生成等子过程。

注意

注意编译(compilation)这个词一般的含义是将高级语言代码转化为二进制程序,但是如果在整个编译流程中谈到这个词,则需要将其与汇编(assemble)进行区分:

  • 编译是将高级语言代码转化为汇编代码
  • 汇编是将汇编代码转化为二进制代码

汇编

汇编阶段负责将汇编代码(assembly code)转换为目标文件(object file)。汇编器(assembler)解析汇编指令,将其翻译为对应的机器码。

链接

Source
File
Source
File
Source
File
Source
File
Object
File
Object
File
Object
File
Object
File
Linker
Rumtime
Library
Executable
File

如上图所示,链接器(linker)的作用是将由编译器生成的一个或多个目标代码文件(object file,通常是汇编器生成的机器代码)合并为一个单一的可执行文件。在这个过程中,链接器主要完成如下任务:

  1. 符号解析:查找所有未定义的符号(如函数调用、全局变量)并找到对应的定义。
  2. 重定位:确定目标文件中的符号地址,并更新相关指令或数据。
  3. 合并代码和数据段:将不同目标文件的代码和数据合并,形成最终的可执行文件。

链接分为静态链接和动态链接两种方式。

Program A
Static
Library
Program B
Static
Library
Program A
Program B
Shared
Library
Static Linking
Dynamic Linking
  • 静态链接:静态链接是在编译时将所有依赖的库代码拷贝到最终的可执行文件中,生成一个完全独立的二进制文件。
  • 动态链接:动态链接不会在编译时将库代码合并,而是在运行时加载外部共享库(.so / .dll)。可执行文件只包含对库的引用,而不包含库的代码。

汇编代码

高级语言程序对应的汇编代码 尝尝与存储系统 在大题中进行综合考察,这里需要重点掌握选择、循环、函数调用语句对应的汇编代码。

选择结构语句

选择语句的基本执行思路:选择语句中的变量被保存在寄存器中,通过条件比较指令对寄存器进行比较,然后跳转到不同的分支执行不同的代码。

选择结构C语言

if (a > b) {
    max = a;
} else {
    max = b;
}
汇编

; 假设a, b的值分别存放在寄存器eax和ebx中
; 比较 a 和 b
cmp eax, ebx
; 如果 a <= b, 跳转到 else_label
jle else_label       
; a > b 的分支,无需跳转
mov max, eax     ; max = a
jmp endif_label  ; 跳转到 endif_label
; a <= b 的分支
else_label:
mov max, ebx     ; max = b
; 执行结束
endif_label:

循环结构语句

选择结构C语言

while (count < 10) {
    count++;
}
汇编

; 假设count的值存放在寄存器ecx中
start:
cmp ecx, 10   ; 比较count和10
jge end       ; 如果count >= 10, 跳出循环
inc ecx       ; count增加
jmp start     ; 无条件跳回循环开始
end:

函数定义和调用

C 语言中函数对应的汇编代码从逻辑上可以分为三个部分:

  1. 函数入口
    • 保存寄存器:保存调用者(caller)的寄存器,以确保在函数执行完后,寄存器的值不被改变。
    • 设置栈帧:保存 caller 的栈帧,设置被调用者(callee)的栈帧。
  2. 函数体
    • 这部分是函数的执行逻辑,会包含各种操作指令,此时局部变量会被保存到栈上。
  3. 函数返回
    • 如果函数有返回值,通常会将结果保存在 eax 寄存器中。
    • 恢复栈帧:恢复栈指针,确保栈帧被正确销毁。
    • 恢复寄存器:如果函数入口时保存了寄存器,那么在返回之前,需要将它们恢复。

这一节可以结合 函数调用时内存结构 共同理解,下面通过几个简单的例子说明以下函数定义和调用。

add函数

int add(int x, int y) {
    return x + y;
}
汇编

; 假定 'a' 和 'b' 作为参数通过堆栈传递
.globl _add
_add:
    ; 保存 caller 的 ebp
    push ebp
    ; 设置 callee 的 ebp
    mov ebp, esp
    ; x + y 的汇编表示
    mov eax, [ebp+8]
    add eax, [ebp+12]
    ; 恢复 caller 的 ebp
    pop ebp
    ; 函数返回
    ret

在 add 函数对应的汇编代码中,首先需要保存 caller(下文中的 func 函数) 的栈帧并设置 callee(即 add 函数自己)的栈帧,这个步骤通过 push ebp 以及 mov ebp, esp 完成。这个例子比较简单,所以寄存器够用,caller 和 callee 的寄存器不会出现竞争的情况,所以无需在 callee 中保存 caller 的寄存器。

add 函数体的工作就是计算 x + y 的结果。

func函数

void func(int a, int b) {
    int sum = add(a, b);
    int var = sum * 2;
    // ... 一些使用var的代码 ...
}
汇编

.globl _func
_func:
    push ebp
    mov ebp, esp
    ; 调用函数 add
    sub esp, 8 
    push dword [ebp+12]
    push dword [ebp+8]
    call _add
    add esp, 8
    ; 保存返回值
    mov [ebp-4], eax
    mov eax, [ebp-4]
    ; 将eax左移1位,相当于乘以2
    shl eax, 1              
    ; 将结果存储到 'var'
    mov [ebp-8], eax        

    ; ... 更多使用 'var' 的代码 ...

    ; 函数完成,清理堆栈,并恢复ebp
    mov esp, ebp
    pop ebp
    ret