通过反汇编分析,我们可以从体系结构和底层汇编这样一个新视角去窥探程序的运行机制,如函数调用、参数传递、内存中堆栈的动态变化等,会对C语言有一个更深的理解

ARM体系结构

  • 计算机的指令集一般可分为4种:复杂指令集(CISC)、精简指令集(RISC)、显式并行指令集(EPIC)和超长指令字指令集(VLIW)。

RISC指令集与CISC指令集的区别

  • Load/Store架构:CPU不能直接处理内存中的数据,要先将内存中的数据Load(加载)到寄存器中才能操作,然后将处理结果Store(存储)到内存中。
  • 固定的指令长度、单周期指令
  • 倾向于使用更多的寄存器存储数据,而不是使用内存中的堆栈,效率更高。

ARM指令集与RISC的区别

ARM指令集虽然属于RISC,但是和原汁原味的RISC相比,还是有一些差异的

  • ARM有桶型移位寄存器,单周期内可以完成数据的各种移位操作
  • 并不是所有的ARM指令都是单周期的。
  • ARM有16位的Thumb指令集,是32位ARM指令集的压缩形式,提高了代码密度。
  • 条件执行:通过指令组合,减少了分支指令数目,提高了代码密度。
  • 增加了DSP、SIMD/NEON等指令。

ARM处理器工作模式

  • 应用程序正常运行时,ARM处理器工作在用户模式(User mode),当程序运行出错或有中断发生时,ARM处理器就会切换到对应的特权工作模式
处理器模式模式编码模式介绍
User mode0B10000应用程序正常运行时的工作模式
FIQ mode0B10001快速中断模式,中断优先级比 IRQ
IRQ mode0B10010中断模式
Supervisor mode0B10011管理模式,保护模式,复位软中断时一般都会进入该模式
Abort mode0B10111数据存取异常、指令读取失败时会进入该模式
Undefined mode0B11011CPU 遇到无法识别、未定义的指令时,会进入该模式
System mode0B11111类似用户模式,但可运行特权 OS 任务,如切换到其他模式
Monitor mode0B10110仅限于安全扩展
  • 处理器处于普通模式,没有权限对内存和底层硬件进行操作
  • 首先通过系统调用软中断进入处理器特权模式,运行操作系统内核或硬件驱动代码,才能对底层的硬件设备进行读写操作。

寄存器

  • ARM处理器内部,除了基本的算术运算单元、逻辑运算单元、浮点运算单元和控制单元,还有一系列寄存器,包括各种通用寄存器、状态寄存器、控制寄存器,用来控制处理器的运行,保存程序运行时的各种状态和临时结果

寄存器分类

ARM处理器中的寄存器可分为通用寄存器和专用寄存器两种

  • R0~R12属于通用寄存器
  • 除了FIQ工作模式,在其他工作模式下这些寄存器都是共用、共享
  • R0~R3通常用来传递函数参数R4~R11用来保存程序运算的中间结果或函数的局部变量等,R12常用来作为函数调用过程中的临时寄存器

ARM处理器有多种工作模式,除了这些在各个模式下通用的寄存器,还有一些寄存器在各自的工作模式下是独立存在的,如R13R14R15CPSPSPSR寄存器,在每个工作模式下都有自己单独的寄存器

  • R13寄存器又称为堆栈指针寄存器(Stack Pointer,SP),用来维护和管理函数调用过程中的栈帧变化R13总是指向当前正在运行的函数的栈帧,一般不能再用作其他用途
  • R14寄存器又称为链接寄存器(Link Register,LR),在函数调用过程中主要用来保存上一级函数调用者的返回地址
  • 寄存器R15又称为程序计数器(Program Counter,PC)​,CPU从内存取指令执行,就是默认从PC保存的地址中取的,每取一次指令,PC寄存器的地址值自动增加
  • 在ARM三级流水线中,PC指针的值等于当前正在运行的指令地址+8
  • 当前处理器状态寄存器Current Processor State Register,CPSR)主要用来表征当前处理器的运行状态。
  • 在每种工作模式下,都有一个单独的程序状态保存寄存器Saved Processor State Register,SPSR)

ARM处理器切换工作模式或发生异常

  • SPSR用来保存当前工作模式下的处理器现场,即将CPSR寄存器的值保存到当前工作模式下的SPSR寄存器。

从异常返回时

  • SPSR寄存器中恢复原先的处理器状态,切换到原来的工作模式继续运行
  • FIQ模式。为了快速响应中断,减少中断现场保护带来的时间开销,在FIQ工作模式下,ARM处理器有自己独享R8~R12寄存器。

ARM汇编指令

一个完整的ARM指令通常由操作码 + 操作数组成

存储访问指令

ARM指令集属于RISC指令集

  • RISC处理器采用典型的加载/存储体系结构,CPU无法对内存里的数据直接操作,只能通过Load/Store指令来实现
  • ARM处理器属于冯·诺依曼架构,程序和数据都存储在同一存储器上,内存空间和I/O空间统一编址,ARM处理器对程序指令、数据、I/O空间中外设寄存器的访问要通过Load/Store指令来完成。
LDR R1, [R0]; 将R0中的值作为地址,将该地址上的数据保存到R1
 
STR R1, [R0]; 将R0中的值作为地址,将R1中的值存储到这个内存地址
 
LDRB / STRB; 每次读写一字节,LDR/STR 默认每次读写四字节
 
LDM / STM; 批量装载/存储指令,在一组寄存器和一片内存之间传输数据
 
SWP R1, R1, [R0]; 将R1与R0中地址指向的内存单元中的数据进行交换
 
SWP R1, R2, [R0]; 将R0存储到R1,将R2写入[R0]这个内存存储单元

LDR/STR、LDM/STM

LDM/STM指令常用来加载或存储一组寄存器到一片连续的内存,通过和堆栈格式符组合使用,LDM/STM指令还可以用来模拟堆栈操作

堆栈格式说明备 注
FAFull Ascending满递增堆栈
FDFull Descending满递减堆栈
EAEmpty Ascending空递增堆栈
EDEmpty Descending空递减堆栈

ARM处理器使用的一般都是满递减堆

  • 在一个堆栈内存结构中,如果堆栈指针SP总是指向栈顶元素,那么这个栈就是满栈;
  • 如果堆栈指针SP指向的是栈顶元素的下一个空闲的存储单元,那么这个栈就是空栈。
  • 如果栈指针SP从高地址往低地址移动,那么这个栈就是递减栈;

在入栈和出栈过程中要留意栈中各个元素的入栈出栈顺序

  • 将一组寄存器入栈,或者从栈中弹出一组寄存器:
LDMFD SP!,{R0-R2,R14} ; 将内存栈中的数据依次弹出到 R14,R2,R1,R0
STMFD SP!,{R0-R2,R14} ; 将R0,R1,R2,R14依次压入内存栈

元素在入栈操作时,STMFD会根据大括号{}中寄存器列表中各个寄存器的顺序,从左往右依次压入堆栈

ARM还专门提供了PUSHPOP指令来执行栈元素的入栈和出栈操作。PUSH和POP指令的使用方法如下。[插图]

PUSH {R0-R2,R14} ; 将R0、R1、R2、R14依次压入栈
POP {R0-R2,R14} ; 将栈中的数据依次弹出到 R14、R2、R1、R0

数据传输指令

  • LDR/STR指令用来在寄存器内存之间输送数据
  • 寄存器之间传送数据,则可以使用MOV指令
MOV R1, #1 ;将立即数1发送到寄存器R1中
MOV R1, R0 ;将R0寄存器中的值传送到R1寄存器中
MOV PC, LR ;子程序返回
MVN R0, #0xFF ;将立即数0xFF取反后赋值给R0
MVN R0, R1 ;将R1寄存器的值取反后赋值给R0

比较指令

比较指令用来比较两个数的大小,或比较两个数是否相等

  • 比较指令的运算结果会影响CPSR寄存器的NZCV标志位

条件执行指令

条件执行经常出现在跳转或循环的程序结构中

跳转指令

BL跳转指令表示带链接的跳转

  • 在跳转之前,BL指令会先将当前指令的下一条指令地址(即返回地址)保存到LR寄存器中,然后跳转到label处执行。
  • 子函数执行结束后,LR寄存器中的地址被赋值给PC,处理器就可以返回到原来的主函数中继续运行

BX表示带状态切换的跳转

  • Rm寄存器中保存的是跳转地址,要跳转的目标地址处可能是ARM指令,也可能是Thumb指令
  • 处理器根据Rm[0]位决定是切换到ARM状态还是切换到Thumb状态。

BLX指令是BL指令和BX指令的综合,表示带链接和状态切换的跳转

ARM寻址方式

  • ARM属于RISC体系架构,一个ARM汇编程序中的大部分汇编指令,基本上都和数据传输有关:在内存-寄存器、内存-内存、寄存器-寄存器之间来回传输数据。

寻址方式

  • 比较常见的寻址方式有寄存器寻址、立即寻址、寄存器偏移寻址、寄存器间接寻址、基址寻址、多寄存器寻址、相对寻址等。

寄存器寻址

  • 操作数保存在寄存器中,通过寄存器名就可以直接对寄存器中的数据进行读写
MOV R1, R2 ;;将寄存器 R2 中的值传递到 R1
ADD R1, R2, R3 ;;运行减法运算 R2-R3,并将结果保存到 R1 中

立即数寻址

  • 在立即数寻址中,ARM指令中的操作数为一个常数。立即数以为前缀,0x前缀表示该立即数为十六进制,不加前缀默认是十进制
ADD R1, R1, #1 ;将R1寄存器中的值加1,并将结果保存到R1中
MOV R1, #0xFF ;将十六进制常数0xFF写入R1寄存器中
MOV R1, #12 ;将十进制常数12放入R1寄存器中
ADD R1, R1, #16, 20 ;R1 = R1 + 立即数16循环右移20位

寄存器偏移寻址

  • 寄存器偏移寻址可以看作寄存器寻址的一种特例
MOV R2, R1, LSL #3 ;R2 = R1 << 3
ADD R3, R2, R1, LSL #3 ;R3 = R2 + R1 << 3
ADD R3, R2, R1, LSL R0 ;R3 = R2 + R1 << R0

常见的移位操作有逻辑移位和算术移位

  • 逻辑移位无论是左移还是右移,空缺位一律补0;而算术移位则不同,左移时空缺位补0,右移时空缺位使用符号位填充。

寄存器间接寻址

寄存器间接寻址主要用来在内存和寄存器之间传输数据

  • 寄存器中保存的是数据在内存中的存储地址,我们通过这个地址就可以在寄存器和内存之间传输数据
  • C语言中的指针操作,在汇编层次其实就是使用寄存器间接寻址实现的
LDR R1, [R2] ;将R2中的值作为地址,取该内存地址上的数据,保存到R1
STR R1, [R2] ;将R1寄存器的值写入该内存地址

基址寻址

  • 基址寻址其实也属于寄存器间接寻址。两者的不同之处在于,基址寻址将寄存器中的地址与一个偏移量相加,生成一个新地址,然后基于这个新地址去访问内存。
  • 基址寻址一般用在查表、数组访问、函数的栈帧管理等场合
LDR R1, [FP, #2] ;将FP中的值加2作为新地址,取该地址上的值保存到R1
LDR R1, [FP, #2]!;FP=FP+2,然后将FP指定的内存单元数据保存到R1中
LDR R1, [FP, R0] ;将FP+R0作为新地址,取该地址上的值保存到R1
LDR R1, [FP, R0, LSL #2] ;将FP+R0<<2作为新地址,读取该内存地址上的值保存到R1
LDR R1, [FP], #2 ;将FP中的值作为地址,读取该地址上的值保存到R1,然后FP中的值加2
STR R1, [FP, #-2];将FP中的值减2,作为新地址,将R1中的值写入该地址
STR R1, [FP], #-2;将FP中的值作为地址,将R1中的值写入此地址;然后FP中的值减2

多寄存器寻址

  • STM/LDM指令就属于多寄存器寻址,一次可以传输多个寄存器的值
LDMIA SP!, {R0-R2,R14} ;将内存栈中的数据依次弹出到 R14,R2,R1,R0
STMDB SP!, {R0-R2,R14} ;将R0,R1,R2,R14依次压入内存栈
LDMFD SP!, {R0-R2,R14} ;将内存栈中的数据依次弹出到 R14,R2,R1,R0
STMFD SP!, {R0-R2,R14} ;将R0,R1,R2,R14依次压入内存栈

连续的寄存器,还可以使用连接符-连接

  • 栈是程序运行过程中非常重要的一段内存空间,栈是C语言运行的基础,函数内的局部变量、函数调用过程中要传递的参数、函数的返回值一般都是保存在栈中的
  • 在嵌入式系统的一些启动代码中,在运行C语言程序之前,必须要先运行一段汇编代码初始化内存和栈指针SP,然后才能跳到C语言程序中运行。

ARM没有专门的入栈和出栈指令,ARM中的栈操作其实就是通过上面所讲的STM/LDM指令和栈指针SP配合操作完成的

  • ARM默认使用满递减堆栈
  • STMFD/LDMFD指令配对使用,完成堆栈的入栈和出栈操作
  • ARM中的PUSHPOP指令其实就是LDM/STM的同义词,是LDMFDSTMFD组合指令的助记符。
STMFD SP!, {R0-R2,R14} ;将R0、R1、R2、R14依次压入内存栈
LDMFD SP!, {R0-R2,R14} ;将内存栈中的数据依次弹出到 R14、R2、R1、R0
PUSH {R0-R2,R14} ;将R0、R1、R2、R14依次压入栈
POP {R0-R2,R14} ;将栈中的数据依次弹出到 R14、R2、R1、R0

相对寻址

相对寻址其实也属于基址寻址

  • 是基址寻址的一种特殊情况。它是以PC指针作为基地址进行寻址的,以指令中的地址差作为偏移,两者相加后得到的就是一个新地址
  • 很多与位置无关的代码,如动态链接共享库,其在汇编代码层次的实现其实也是采用相对寻址的
  • 程序中使用相对寻址访问的好处是不需要重定位,将代码加载到内存中的任何地址都可以直接运行。

3.4 ARM伪指令

ARM伪指令并不是ARM指令集中定义的标准指令,而是为了编程方便,各家编译器厂商自定义的一些辅助指令

  • 伪指令有点类似C语言中的预处理命令,在程序编译时,这些伪指令会被翻译为一条或多条ARM标准指令
  • 常见的ARM伪指令主要有4个:ADRADRLLDRNOP
ADR R0, LOOP ;将标号LOOP的地址保存到R0寄存器中
ADRL R0, LOOP ;中间范围的地址读取
LDR R0, =0x30008000 ;将内存地址0x30008000赋予R0
NOP ;空操作,用于延时或插入流水线中暂停指令的运行
  • NOP伪指令比较简单,其实就相当于MOV R0,R0。

LDR伪指令

  • ARM属于RISC架构,不能对内存中的数据直接操作,ARM通常会使用LDR/STR这对加载/存储指令,先将内存中的数据加载到寄存器,然后才能对寄存器中的数据进行操作,最后把寄存器中的处理结果存储到内存中
  • LDR伪指令的主要用途是将一个32位的内存地址保存到寄存器中。
  • RISC指令的特点是单周期指令,指令的长度一般都是固定的。在一个32位的系统中,一条指令通常是32位的,指令中包括操作码和操作数, ARM指令的编码格式指令中的操作码和操作数共享32位的存储空间:一般前面的操作码要占据几个比特位,剩下来的留给操作数的编码空间就小于32位。
  • 为了与ARM指令集中的加载指令LDR区别开来,LDR伪指令中的操作数前一般会有一个等于号=,用来表示该指令是个伪指令。

通过LDR伪指令,编译器就解决了向一个寄存器传送32位的立即数时指令无法编码的难题

  • 在程序编译期间,这些伪指令会被标准的ARM指令替代。编译器在处理伪指令时,根据伪指令中的操作数大小,会使用不同的ARM标准指令替代。
  • 存放这些32位地址常量的文字池一般紧挨着当前指令的代码段,直接放置在当前代码段的后面。
LDR R0, =0x30008000 ;将立即数0x30008000送入R0
LDR R0, =LOOP ;将标号LOOP表示的地址送入R0
LDR R0, [R1] ;将R1中的值作为地址,取该地址上的值送入R0
LDR R0, [PC, #LOOP] ;将标号LOOP表示的内存地址上的数据送入R0

ADR伪指令

ADR伪指令的功能与LDR伪指令类似,将基于PC相对偏移的地址值读取到寄存器中。

  • ADR为小范围的地址读取伪指令,底层使用相对寻址来实现,因此可以做到代码与位置无关

ADR伪指令和LDR伪指令

  • 两者都是为了加载一个地址到指定的寄存器中
  • LDR伪指令通常被翻译为ARM指令集中的LDRMOV指令
  • ADR伪指令则通常会被ADDSUB指令代替。

用途上

  • LDR伪指令主要用来操作外部设备的寄存器
  • ADR伪指令主要用来通过相对寻址,生成与位置无关的代码
  • LDR使用绝对地址,而ADR则使用相对地址
  • LDR和ADR伪指令的地址适用范围也不同,LDR伪指令适用的地址范围为[0,32GB]​,而ADR伪指令则要求当前指令和标号必须在同一个段中,地址偏移范围也较小,地址对齐时偏移范围为[0,1020]​,地址未对齐时偏移范围为[0,4096]​

ARM汇编程序设计

ARM汇编程序是以段(section)为单位进行组织的

  • 在一个汇编文件中,可以有不同的section,分为代码段、数据段等,各个段之间相互独立
  • AREA伪操作来标识一个段的起始、段名、段的属性(CODEDATA)和读写权限(READONLYREADWRITE)​。
  • ARM汇编程序通过ENTRY这个伪操作来标识汇编程序的运行入口,使用伪操作END来标识汇编程序的结束。
  • ARM汇编程序中可以使用标号。像C语言一样,在汇编语言中,标号代表的指令地址
  • 在汇编程序中使用分号来注释代码。在一个空行的行首或者一条指令语句的末尾添加一个分号,然后就可以在分号后面添加注释,以增加程序的可读性。

符号与标号

使用符号来标识一个地址、变量或数字常量。当用符号来标识一个地址时,这个符号通常又被称为标号

  • 符号的命名规则和C语言的标识符命名规则一样:由字母、数字和下画线组成,符号的开头不能使用数字,但标号除外
  • 符号的命名在其作用域内必须唯一,不能与系统内部或系统预定义的符号同名,不能与指令助记符、伪指令同名,作用域是整个汇编源文件
  • 直接通过数字[0,99]而不是使用字符来进行地址引用,我们称这种数字为局部标号。局部标号的作用域为当前段

伪操作

  • 在汇编语言中,为了编程方便,汇编器也定义了一些特殊的指令助记符,以方便对汇编程序做各种处理。如使用AREA来定义一个段(section)​,使用GBLA来定义一个数据,使用ENTRY来指定汇编程序的执行入口等,这些指令助记符统称为伪指令或伪操作
GBLA a ;定义一个全局算术变量a,并初始化为0
a SETA 10 ;给算术变量a赋值为10
GBLL b ;定义一个全局逻辑变量b,并初始化为{false}
b SETL 20 ;给逻辑变量b赋值为20
GBLS STR ;定义一个全局字符串变量STR,并初始化为0
STR SETS "zhaiXue.cc" ;给变量STR赋值为"zhaiXue.cc"
LCLA a ;定义一个局部算术变量a,并初始化为0
LCLL b ;定义一个局部逻辑变量b,并初始化为{false}
LCLS name ;定义一个局部字符串变量name,并初始化为0
name SETS "wanglitao";给局部字符串变量赋值
  • 关于数据定义,常用的伪操作有DCD、DCB、SPACE、DATA
DATA1 DCB 10,20,30,40 ;分配一片连续的字节存储单元并初始化
STR   DC    "zhaiXue.cc" ;给字符串分配一片连续的存储单元并初始化
DATA2 DCB 10,20,30,40 ;分配一片连续的字存储单元并初始化
BUF   SPACE 100 ;给 BUF 分配 100 字节的存储单元并初始化为 0

C语言和汇编语言混合编程

  • 在ARM启动代码中,系统一上电首先运行的是汇编代码,等初始化好内存堆栈环境后,才会跳到C程序中执行。

ATPCS

  • ATPCS的全称是ARM-Thumb Procedure Call Standard,其核心内容就是定义了ARM子程序调用的基本规则及堆栈的使用约定等。
  • ATPCS规定了ARM程序要使用满递减堆栈,入栈/出栈操作要使用STMFD/LDMFD指令

ATPCS最重要的内容是定义了子程序调用的具体规则

  • 子程序间要通过寄存器R0~R3(可记作a0~a3)传递参数,当参数个数大于4时,剩余的参数使用堆栈来传递。
  • 子程序通过R0~R1返回结果。
  • 子程序中使用R4~R11(可记作v1~v8)来保存局部变量
  • R12作为调用过程中的临时寄存器,一般用来保存函数的栈帧基址,记作FP
  • R13作为堆栈指针寄存器,一般记作SP
  • R14作为链接寄存器,用来保存函数调用者的返回地址,记作LR
  • R15作为程序计数器,总是指向当前正在运行的指令,记作PC
  • 为了能在C程序中内嵌汇编代码,ARM编译器在ANSI C标准的基础上扩展了一个关键字__asm。通过这个关键字,我们就可以在C程序中内嵌ARM汇编代码
__asm      /* 开始汇编代码块 */
{
    指令     /* 我是注释 */
    ...
    [指令]
}
  • 如果在内嵌的汇编代码中添加注释,记得要使用C语言的/**/注释符,而不是汇编语言的分号注释符。
  • 不同的编译器基于ANSI C标准扩展了不同的关键字,使用的汇编格式可能也不太一样
  • GNU ARM编译器提供了一个__asm__关键字(使用__volatile__关键字修饰,用来告诉编译器不要优化)
__asm__ __volatile__(
    "汇编语句;"
    ...
    "汇编语句;"
);

GNU ARM汇编语言

  • 编译器:用来将C源文件编译成汇编文件。
  • 汇编器:用来将汇编文件汇编成目标文件。
  • 链接器:用来将目标文件组装成可执行文件。
  • 二进制转化工具:objdumpobjcopystrip等。
  • 库打包工具:ar。
  • 调试工具:gdb、nm。
  • 库/头文件:根据C语言标准定义的API实现的C标准库及对应的头文件。

GNU ARM编译器的伪操作

ARM 编译器GNU ARM 编译器伪操作说明
AREA copy, CODE,….text定义一个代码段
AREA , dat, DATA,….data定义一个数据段
使用;注释使用 /*/ 或 @ 注释 汇编程序中的注释方式
DCD.long.word分配一片连续的字存储单元
EntryENTRY(_start)汇编程序的执行入口
END.end汇编程序的结束标记
CODE32.arm / .code 32告诉编译器后面指令为ARM指令
CODE16.thumb / .code 16告诉编译器后面指令为THUMB指令
SPACE.space分配一片连续的内存并初始化为0
GBLL、GBLA.global定义一个全局变量
EXPORT、GLOBAL.global全局符号声明,可以被其他文件引用
IMPORT、EXTERN.extern引用其他文件的全局符号前要先声明
EQU、SETL、SETA.equ_set赋值语句,为一个变量赋值
IF、ELSE、ENDIF.ifdef.else .endif条件汇编
MACRO / MEND.macro / .endm宏定义
GET INCLUDE.include文件包含,并展开编译
INCBIN.incbin文件包含,不编译
  • GNU ARM汇编语言中的标识符可以由字母、数字、下画线和.构成,局部标号可以由纯数字构成
  • GNU ARM汇编语言使用标号_start作为汇编程序的入口

.section伪操作

  • 在GNU ARM汇编语言中,用户可以使用.section伪操作自定义一个段
.section ".mysection", "awx" 	// 定义一个可写、可执行的段
.align 2
  • 在使用伪操作.section定义一个段时,每个段以段名开始,以下一个段名或文件结尾作为结束标记

基本数据格式

  • 二进制数据通常以0B0b开头,八进制数据以0开头,十六进制数据以0x开头,十进制数据则以非0数字开头。负数前面加“-”​,取补用“”​,不相等用“<>”​,其他运算符号如+、-、*、%、<、<<、>、>>、|、&、^、!、==、>=、&&与C语言语法相似。
  • 使用.ascii定义字符串时要自行在结尾加'\0'
  • 使用.string伪操作可以定义多个字符串
  • 使用.asciz伪操作可以定义一个以NULL字符结尾的字符串
  • 使用.rept伪操作可以重复定义数据。

在GNU ARM汇编程序中经常使用小圆点.表示当前指令的地址

数据定义

  • 在GNU ARM汇编程序中,如果我们想定义一个浮点数
标签 f:
.float 3.14
.equ f, 3.1415
  • 使用.float伪操作定义一个浮点数f,并初始化为3.14。如果你想将这个浮点数重新赋值为3.1415,则可以通过.equ伪操作来完成
  • .equ伪操作除了给数据赋值,还可以把常量定义在代码段中,然后在代码中直接引用。类似C语言中的#define宏定义。