知识导图 知识导图
首页
iOS知识
计算机软件
  • 即时通讯网 (opens new window)
  • 开发常用网站 (opens new window)
首页
iOS知识
计算机软件
  • 即时通讯网 (opens new window)
  • 开发常用网站 (opens new window)
  • MD

    • markdown常用语法
  • Vue

  • C语法

  • C++语法

  • 汇编语言

  • 软件编程及算法
  • 汇编语言
2023-07-25
目录

intel-32基础语法

# 前言

汇编语言多种多样,不同于其他语言,汇编指令对接的是硬件,所以不同厂家,甚至相同的厂家的不同芯片都有不同的汇编指令集。

因此花时间学完所有的汇编指令显得不必要,只要对主流的汇编语法学习然后融会贯通即可。

常见学习的汇编:

1、8086汇编,intel-16,早期较为基础的汇编指令,也是入门常用的汇编学习版本,大学教材《王爽汇编xx》

2、intel-32位 ,intel的32位芯片

3、intel-64,现在主流的intel芯片版本

4、ARM-64,ARM当前主流的版本

⚠️ 注意:

本篇以C++语法对比介绍intel-32汇编指令集(无特殊说明的,下面介绍的汇编指令均为 intel-32 指令集)

# 基本结构

# Hello World

Hello World 示例对比

1、C++ 显示字符串“ Hello World”

#include <iostream> // 引入支持输入输出的库
using namespace std; // 命名空间

int main(){ // 程序开始执行入口函数
    cout << "Hello World"; // cout 输出
    return 0; 
} 

C++在线编译(菜鸟工具) (opens new window)

2、intel32汇编语言显示字符串“ Hello World”

section .text
   global _start   

_start:          
   mov  edx,len   
   mov  ecx,msg    
   mov  ebx,1      
   mov  eax,4       
   int  0x80       

   mov  eax,1      
   int  0x80     

section .data
msg db 'Hello, world!', 0xa  
len equ $ - msg    

链接:intel32汇编在线编译 (opens new window)

上面结果都是打印一个 'Hello, world!' 但是,

显然汇编语法较为复杂,下面将先从结构上介绍他们的区别。

(具体 ax、bx、cx、dx、int 等指令后面再介绍)

# 语法结构

改造C++ Hello World示例语法:

#include <iostream> // 引入支持输入输出的库
using namespace std; // 命名空间

// 1、data段: 被定义不可修改的数据常量
const string msg = "Hello World";

// 2、text段:程序代码
int main();

int main(){ // 3、程序入口函数
	
    cout << msg; // cout 输出
		// 4、程序返回
    return 0; // ret
} 

汇编代码: Hello World

; 1、data段: 被定义不可修改的数据常量
section	.data
msg db 'Hello, world!', 0xa  ;要打印的字符串
len equ $ - msg     ;字符串的长度,尾地址($)- 起始地址(msg[0])= 字符串长度 

; 2、text段:程序代码
section	.text
   global _start     ;必须为链接器(ld)声明

_start:	            ; 3、程序入口函数:告诉链接器入口点
   mov	edx,len     ;消息长度
   mov	ecx,msg     ;写消息
   mov	ebx,1       ;文件描述符 (stdout)
   mov	eax,4       ;系统调用号 (sys_write)
   int	0x80        ;调用内核

   mov	eax,1       ;系统调用号 (sys_exit)
   ; 4、程序中断 
   int	0x80        ;调用内核

对比两段代码(大概类比,方便记忆):

1、data段:

被定义不可修改的数据常量,其中数据 **msg db ***** 等同于C++ 常量 const string msg

2、text段:声明

程序代码,比如入口函数声明 global _start 等同于C++ main函数声明 int main();

3、text段:程序实现

程序入口函数

代码实现 _start: 等同于 C++ main函数 实现 int main(){ *** }

4、text段:执行中断或退出

一个程序的函数通常会在使用完后中断退出

mov eax,1 ;系统调用sys_exit,相当main函数的退出,也即是进程退出

返回:

mov	eax,4
int	0x80 ;调用内核中断

上面两句相当于函数(过程)的返回吧,
return :C++中当前函数执行完毕

5、屏幕输出

cout : c++屏幕输出

mov ebx,1 ;文件描述符 (stdout)

6、注释

intel32汇编语言 行注释以分号(;)开头

; 注释信息

C++中行注释

// 注释信息

(还有其他多种注释方法)

# intel32程序三个段结构

# 1、data段

数据(data)段被用于声明初始化的数据或常数。此数据在运行时不会更改。您可以在段中声明各种常量值,文件名或缓冲区大小等。

声明数据段的语法是-

section.data

# 2、bss 段

在bss段用于声明变量。声明bss段的语法是-

section .bss           ;未初始化的数据
   num resb 8

# 3、text段

代码段被用于保持实际的代码。该段必须以全局声明**_start**开头,该声明告诉内核程序从何处开始执行。

声明代码段的语法是-

section.text
   global _start
_start:

# 寄存器

# 常见寄存器

寄存器 16位 32位 64位
累加寄存器 accumulator AX EAX RAX
基址寄存器 base BX EBX RBX
计数寄存器 count CX ECX RCX
数据寄存器 data DX EDX RDX
堆栈基指针 Base Pointer BP EBP RBP
变址寄存器 Source Index SI ESI RSI
堆栈顶指针 Stack Pointer SP ESP RSP
指令寄存器 Instruction Pointer IP EIP RIP
AH&AL=AX(accumulator):累加寄存器 
BH&BL=BX(base):基址寄存器 
CH&CL=CX(count):计数寄存器 
DH&DL=DX(data):数据寄存器 
SP(Stack Pointer):堆栈指针寄存器 
BP(Base Pointer):基址指针寄存器 
SI(Source Index):源变址寄存器 
DI(Destination Index):目的变址寄存器 
IP(Instruction Pointer):指令指针寄存器 
CS(Code Segment)代码段寄存器 
DS(Data Segment):数据段寄存器 
SS(Stack Segment):堆栈段寄存器 
ES(Extra Segment):附加段寄存器 

⚠️

1、你会发现寄存器指令名称基本是在16位基础上衍生而来,比如32位 EAX是在16 位AX前加E,64 位 RAX是在16 位AX前加R

2、H是高位缩写,L是低位缩写。比如 AX 由AH 高位和AL低位组合,这是为兼容8位寄存器而来

3、篇头demo中下面代码什么意思?

_start:	            ; 3、程序入口函数:告诉链接器入口点
   mov	edx,len     ;消息长度
   mov	ecx,msg     ;写消息
   mov	ebx,1       ;文件描述符 (stdout)
   mov	eax,4       ;系统调用号 (sys_write)
   int	0x80        ;调用内核

拆分解释一下:

; 程序入口函数:告诉链接器入口点,加这个好理解,就是main函数
_start:	            
   
   ;比如C语法中 int a = 1; 就默认处理好了变量a的数据长度,通常是4字节,代码底层帮你申请了对应大小的空间,通常使用 alloc 分配。
   ;而在汇编中可以通过 dx 告诉编译器当前对象的需要分配的内存大小,
   mov	edx,len     ;消息长度
   
   ; int a = 1 中,赋值1给变量a的过程在汇编中可以使用 cx 这个指令
   ; mov 赋值,msg 数据, cx 指向真实的物理地址,将内容写入的地方
   mov	ecx,msg     ;写消息
   
   ;这个就是打印指令,也即是输出指令,cout、print、等等类似
   ;在bx寄存器中写入1就是输出信息
   mov	ebx,1       ;文件描述符 (stdout)
   
   ;ax中写入100(4)即是相当于函数的返回指令,告诉CPU下面函数过程要做的动作是执行后返回
   mov	eax,4       ;系统调用号 (sys_write)
   
   ;int 不是c中的int,而是 Interrupt,中断的意思,而0x80是intel-32中调用linux中系统命令,可以理解为system_call,也就是系统函数调用
   int	0x80        ;调用内核

# 汇编命令

列举几个汇编命令:

指令 作用
ORG 在程序执行的时候,会告诉编译器将这些指令转载到内存的哪个位置
JMP (jump) 跳转。类似于C中的 go to
entry() JMP 指令的跳转目的地
DB(data byte) 向文件中直接写入一个字节的指令
DW(data word) 写入一个字,两个字节
DD(data double-word) 写入两个字,4个字节
RESB(reserve byte) 与 DB相反,指定的地址清空,保留备用
RESW(reserve data double-word) 与 DW相反
MOV(move) 移动、赋值,等等动作
ADD(add) 加法指令
CMP(compare) 比较指令。
INT(Interrupt) 中断指令,用来调用BIOS中函数的指令
HLT(halt) 停止。让CPU进入待机状态,只要外部发生变化,CPU就会醒过来。
JC(jump if carry) 标志,INF调用函数后,没有错,进位标志为0,有错,进位标志为1
JNC(jump if not carry) 判断指令:进位为0就跳转
JAE(jump if above or equal) 判断指令:大于等于时跳转
JBE(jump if below or equal) 判断指令:小于等于则跳转
RET(return) 返回
XCHG (exchange) 交换指令
push 栈:进栈,或叫压栈指令
pop 栈:出栈指令
sub、sbb、dec、neg、cmp 减法指令
mul、imul 乘法指令
div、idiv 除法指令
not、and、or、xor、test 逻辑运算指令
ROL、ROR、RCL、RCR 循环移位指令
LOOP、LOOPE/LOOPZ、LOOPNE/LOOPNZ、JCXZ 循环指令
其他详见汇编指令梳理篇章

# 变量

# define:变量内存分配、创建(alloc)

NASM提供了各种定义指令来为变量保留存储空间。

define assembler指令用于分配存储空间。

在C++中:

int a = 3;  
double b = 1.345;
byte z = 22;  
char x = 'x'; 

而汇编中,变量示例:

str          DB      'xyz'
num          DW      354534
neg_num      DW      -323
big_num      DQ      119287337644
real_num     DD      1.23456
real_num2    DQ      123.456

其中内存分配的define指令:

指令 目的 储存空间
DB 定义字节 分配1个字节
DW 定义字 分配2个字节
DD 定义双字 分配4个字节
DQ 定义四字 分配8个字节
DT 定义十个字节 分配10个字节

# 示例:输出字符串 xyz

section .text
   global _start          ;链接器入口声明
        
_start:                   ;链接器执行入口
   mov  edx,3             ;定义长度
   mov  ecx,str1          ;写入数据
   mov  ebx,1             ;基地址(base)寄存器, 在内存寻址时存放基地址
   mov  eax,4             ;输出信息
   int  0x80              ;中断

   mov  eax,1             ;退出exit
   int  0x80              ;中断

section .data
str1 DB 'xyz'

结果:

xyz

1、字符的每个字节以十六进制形式存储为其ASCII值

2、数据类型转换为其等效的16位二进制数,并以十六进制数形式存储

3、使用小尾存储顺序(高字节存放内存高位,低字节在低位)

4、负数以补码形式表示

# reserve:变量内存释放(release)

相应的,上面define给对应内存分配空间,而reserve相反,是给对应内存地址清理空间,可以理解为 release

reserve指令用于为未初始化的数据保留空间。reserve指令采用单个操作数,该操作数指定要保留的空间单位数。每个define指令都有一个相关的reserve指令。

保留指令有五种基本形式-

指令 目的
RESB 保留一个字节
RESW 保留字
RESD 保留双字
RESQ 保留四字
REST 保留十个字节

# 示例:申请空间并打印

section	.text
   global _start     ;必须为链接器(ld)声明
   
_start:	            ;告诉链接器入口点
	  
	  ;1、打印固定字符串 “我的年龄:”
    mov	edx,len     ;设置数据长度
    mov	ecx,msg     ;赋值数据
    mov	ebx,1       ;打印信息
    mov	eax,4       ;执行函数标记4
    int	0x80        ;调用内核call函数执行

	  ;2、打印存储后的数据 “9”
    mov  eax,'9'
    mov  [sum], eax ;将数据9存储到申请的内存中
    
    mov	edx,1     
    mov	ecx,sum  ;从 sum中取数据存入
    mov	ebx,1       
    mov	eax,4      
    int	0x80        
		
		;3、退出
    mov	eax,1       ;退出进程sys_exit
    int	0x80        ;调用内核call函数执行退出

section	.data
msg db '我的年龄:',0xA,0xD   ;换行 0xA,0xD 。要打印的字符串
len equ $ - msg     ;字符串的长度

segment .bss
sum resb 1  ;申请1字节空间

结果

我的年龄:
9

# 常量

在C++中我们使用过 const、#define 等 定义一个常量,汇编中类似

# 常量定义

intel 32汇编指令 C++ 含义
EQU (equ) const (比如:const int a = 1;) 不可变常量赋值
%assign static 静态变量,可修改
%define #define 单行宏,代码替换
%macro - %endmacro 多行宏,代码替换

多行宏:

%macro  宏名称 参数数量                      
        push    ebp                
        mov     ebp,esp                
        sub     esp,%1               
%endmacro

指令区分大小写

# 示例

1、C++示例:

#include <iostream>
using namespace std;

// 常量
const int i_x = 3;
// 宏
#define s_y "hello"
// 静态变量
static double d_z = 1.20;

int main()
{
    cout << "不可修改 i_x = " << i_x << endl;
    cout << "不可修改 s_y = " << s_y << endl;
  
    d_z = d_z + 3.14;
    cout << "可修改 d_z = " << d_z << endl;
}

结果

不可修改 i_x = 3
不可修改 s_y = hello
可修改 d_z = 4.34

# 汇编EQU示例

将通用指令固定数据定义为常量,方便使用


sys_exit  	equ 1 ;退出
sys_write 	equ 4 ;函数过程写
sdt_in      equ 0 ;输入
sdt_out    	equ 1 ;输出
sys_call    equ 0x80 ;调用

section  .text
   global _start    
        
_start:            
   mov eax, sys_write         
   mov ebx, sdt_out         
   mov ecx, msg1         
   mov edx, len1 
   int sys_call                
          
   mov eax,sys_exit   
   int sys_call     

section  .data
msg1 db '我是第一行信息!',0xA,0xD   ;换行 0xA,0xD
len1 equ $ - msg1                        

# 汇编宏示例

将上面示例使用宏修改:


;使用字符串变量
%macro  set_string 1                      
        mov ecx, %1          
        mov edx, len1                 
%endmacro

;调用并输出
%macro  run_out 0                      
        mov eax, 4         
         mov ebx, 1 
         int 0x80               
%endmacro

;退出
%macro  return 0                      
        mov eax,1   
         int 0x80              
%endmacro

section  .text
   global _start    
        
_start:      

   set_string msg1 
   run_out
                  
          
   return     

section  .data
msg1 db '使用宏修改!'
len1 equ $ - msg1 

退出的汇编代码直接使用 return 替换,这样更容易记忆。

# 算术指令

# INC 指令

INC指令用于将操作数加1。它对可以在寄存器或内存中的单个操作数起作用。

INC指令具有以下语法-

INC destination

目标操作数可以是8位,16位或32位操作数。

例

INC EBX      ;  32-bit 寄存器 自增1
INC DL       ;  8-bit 寄存器 自增1
INC [count]  ;  变量count  自增1

# DEC指令

DEC指令用于将操作数减1。它对可以在寄存器或内存中的单个操作数起作用。

DEC指令具有以下语法-

DEC destination

操作数目的地可以是8位,16位或32位操作数。

segment .data
   count dw  0
   value db  15
        
segment .text
   inc [count]
   dec [value]
        
   mov ebx, count
   inc word [ebx]
        
   mov esi, value
   dec byte [esi]

# ADD和SUB指令

ADD和SUB指令用于对字节,字和双字大小的二进制数据进行简单的加/减,即分别用于添加或减去8位,16位或32位操作数。

ADD和SUB指令具有以下语法-

ADD/SUB destination, source

ADD / SUB指令可以发生在-

  • 寄存器 to 寄存器
  • 内存 to 寄存器
  • 寄存器 to 内存
  • 寄存器 to 常量数据
  • 内存 to 常量数据

但是,像其他指令一样,使用ADD / SUB指令也无法进行存储器到存储器的操作。ADD或SUB操作设置或清除溢出和进位标志。

下面的示例将要求用户输入两位数字,分别将这些数字存储在EAX和EBX寄存器中,将这些值相加,将结果存储在“ res ”存储位置中,最后显示结果。

SYS_EXIT  equ 1
SYS_READ  equ 3
SYS_WRITE equ 4
STDIN     equ 0
STDOUT    equ 1

segment .data 

   msg1 db "Enter a digit ", 0xA,0xD 
   len1 equ $- msg1 

   msg2 db "Please enter a second digit", 0xA,0xD 
   len2 equ $- msg2 

   msg3 db "The sum is: "
   len3 equ $- msg3

segment .bss

   num1 resb 2 
   num2 resb 2 
   res resb 1    

section .text
   global _start    ;must be declared for using gcc
        
_start:             ;tell linker entry point
   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg1         
   mov edx, len1 
   int 0x80                

   mov eax, SYS_READ 
   mov ebx, STDIN  
   mov ecx, num1 
   mov edx, 2
   int 0x80            

   mov eax, SYS_WRITE        
   mov ebx, STDOUT         
   mov ecx, msg2          
   mov edx, len2         
   int 0x80

   mov eax, SYS_READ  
   mov ebx, STDIN  
   mov ecx, num2 
   mov edx, 2
   int 0x80        

   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg3          
   mov edx, len3         
   int 0x80

   ; moving the first number to eax register and second number to ebx
   ; and subtracting ascii '0' to convert it into a decimal number
        
   mov eax, [num1]
   sub eax, '0'
        
   mov ebx, [num2]
   sub ebx, '0'

   ; add eax and ebx
   add eax, ebx
   ; add '0' to to convert the sum from decimal to ASCII
   add eax, '0'

   ; storing the sum in memory location res
   mov [res], eax

   ; print the sum 
   mov eax, SYS_WRITE        
   mov ebx, STDOUT
   mov ecx, res         
   mov edx, 1        
   int 0x80

exit:    
   
   mov eax, SYS_EXIT   
   xor ebx, ebx 
   int 0x80

尝试一下 (opens new window)

编译并执行上述代码后,将产生以下结果-

Enter a digit:
3
Please enter a second digit:
4
The sum is:
7

具有硬编码变量的程序-

section .text
   global _start    ;must be declared for using gcc
        
_start:             ;tell linker entry point
   mov  eax,'3'
   sub     eax, '0'
        
   mov  ebx, '4'
   sub     ebx, '0'
   add  eax, ebx
   add  eax, '0'
        
   mov  [sum], eax
   mov  ecx,msg 
   mov  edx, len
   mov  ebx,1   ;file descriptor (stdout)
   mov  eax,4   ;system call number (sys_write)
   int  0x80    ;call kernel
        
   mov  ecx,sum
   mov  edx, 1
   mov  ebx,1   ;file descriptor (stdout)
   mov  eax,4   ;system call number (sys_write)
   int  0x80    ;call kernel
        
   mov  eax,1   ;system call number (sys_exit)
   int  0x80    ;call kernel
        
section .data
   msg db "The sum is:", 0xA,0xD 
   len equ $ - msg   
   segment .bss
   sum resb 1

尝试一下 (opens new window)

编译并执行上述代码后,将产生以下结果-

The sum is:
7

⚠️ 认真阅读发现,前面我们示例都是个位数的运算,如果直接换成个位以上的数运算就会得到错误的结果,原因就是溢出,下面在 乘法 中介绍 “进位” 相关的处理

# MUL 乘法指令

1、mul乘法指令是无符号计算的,也就是不支持负数(imul支持负数)

2、mul是单指令,也就是后面只能跟一个参数(imul支持多参)

32 位模式下,MUL(无符号数乘法)指令有三种类型:

1、执行 8 位操作数与 AL 寄存器的乘法; 2、执行 16 位操作数与 AX 寄存器的乘法; 3、执行 32 位操作数与 EAX 寄存器的乘法。

乘积溢出

MUL 指令中的单操作数是乘数。乘积结果存放的空间是被乘数和乘数大小的两倍,因此不会发生溢岀

比如,两个8位二进制数的乘积不会超过16位。两个16位乘积不会超过32位等等(除法会溢出)

MUL无符号、单指令乘法表:

MUL: 无符号乘 ;影响 OF、CF 标志位 ;

指令格式: ;MUL r/m ;参数是乘数 ;

如果参数是 r8/m8, 将把 AL 做乘数, 结果放在 AX ;(8位)

如果参数是 r16/m16, 将把 AX 做乘数, 结果放在 EAX ;(16位)

如果参数是 r32/m32, 将把 EAX 做乘数, 结果放在 EDX:EAX (32位)

被乘数 乘数 乘积存放位置 解析 标记位
AL reg8/mem8 AX MUL操作数是8位寄存器,自动将AL当作被乘数。结果存放AX
AX reg16/mem16 DX:AX MUL操作数是16位寄存器,自动将AX当作被乘数。结果的低位放在AX,高位在DX CF = 1 标记位1表示DX有高位数据。CF = 0 表示DX没有高位数据,AX中已经显示全
EAX reg16/mem32 EDX:EAX MUL操作数是32位寄存器,自动将EAX当作被乘数。结果的低位在EAX,高位在EDX

# 1、8位汇编

8086芯片 intel-8 位汇编个位乘法

MUL(乘法)指令,在C语法中,使用 int a = 2 * 3; 能很方便的计算乘法,而在汇编中却没有这么方便,而只有学习汇编,你才能更深入理解 2 * 3 底层到底是如何实现的

示例:乘积小于10处理


;调用并输出
%macro  run_out 0                      
        mov eax, 4         
        mov ebx, 1 
        int 0x80               
%endmacro

;退出
%macro  return 0                      
        mov eax,1   
        int 0x80              
%endmacro


;乘法:乘积小于10处理 ( num1,num2)
%macro  mul_oper 2                      
         mov  al, %1
         sub  al, '0'

         mov  bl, %2
         sub  bl, '0'
         
         mul  bl  ;这里乘法指令是一种【隐含寻址】方式,下面备注介绍
         add  al, '0'

         mov  [res], al
         mov  ecx,res 
         mov  edx,1                  
%endmacro

section  .text
   global _start    
        
_start:      
   mul_oper '3','3' ;计算3*3结果
   run_out      
   return     

segment .bss
res resb 1

隐含寻址 有些指令的操作码不仅包含了操作的性质,还隐含了部分操作数的地址,如乘法指令MUL,在这条指令中只需指明乘数的地址,而被乘数以及乘积的地址是隐含且固定的。这种将一个操作数隐含在指令码中的寻址方式称为隐含寻址。

例如:

mul  bl ;mul bl的功能把AL中的内容与BL的内容相乘,乘积送到AX寄存器中

;因此上面示例中我们将另一个送入al,然后会自动隐式从 al 取值

# IMUL 乘法指令

imul乘法指令支持符号运算,支持多参数

1、支持正负符号

2、支持2,3个参数,但是三个参数的最后一个必须是立即数

类似的:

; a 16 bit multiplication:
  mov ax, [factor1]
  mov bx, [factor2]
  imul bx              ; 32-bit result in DX:AX
  ; or  imul  word [factor2]

  ; a 32 bit multiplication:
  mov eax, [factor1]
  mov ebx, [factor2] 
  imul ebx             ; 64-bit result in EDX:EAX

在386或更高版本上

	mov   ecx, [factor1]
  imul  ecx, [factor2]    ; result in ecx, no other registers affected
  imul  ecx, ecx          ; and square the result
  
----------------------------  
  
  movsx   ecx, word [factor1]
  movsx   eax, word [factor2]  ; sign-extend inputs to 32-bit
  imul    eax, ecx             ; 32-bit multiply, result in EAX
  imul    eax, eax             ; and square the result
  
----------------------------
  imul  cx, bx, 123        ; requires 186
----------------------------
	imul  ecx, ebx, -123      ; requires 386

# DIV / IDIV指令

# 示例:6/3=2

section .text
   global _start    ;must be declared for using gcc
        
_start:             ;tell linker entry point
   mov  ax,'6'
   sub     ax, '0'
        
   mov  bl, '3'
   sub     bl, '0'
   div  bl
   add  ax, '0'
        
   mov  [res], ax
   mov  ecx,msg 
   mov  edx, len
   mov  ebx,1   ;file descriptor (stdout)
   mov  eax,4   ;system call number (sys_write)
   int  0x80    ;call kernel
        
   mov  ecx,res
   mov  edx, 1
   mov  ebx,1   ;file descriptor (stdout)
   mov  eax,4   ;system call number (sys_write)
   int  0x80    ;call kernel
        
   mov  eax,1   ;system call number (sys_exit)
   int  0x80    ;call kernel
        
section .data
msg db "The result is:", 0xA,0xD 
len equ $- msg   
segment .bss
res resb 1

在线运行上面示例: (opens new window)

# DIV / IDIV (x86 - 32位)

# DIV 无符号除法

DIV 寄存器或内存(8位) DIV 寄存器或内存(16位) DIV 寄存器或内存(32位)

被除数 除数 商 余数
AX reg/mem8 AL AH
DX:AX reg/mem16 AX DX
EDX:EAX reg/mem32 EAX EDX

比例

mov ax, 0043h      ; 被除数
mov bl, 5          ; 除数
div bl             ; AL = 08h, AH = O3h

⚠️

1、除数不能为0,需要判断

2、商或余数存储超过对应空间大小就会抛出异常

3、不占用的寄存器需要清空

DX 包含的是被除数的高位部分,因此在执行 DIV 指令之前,必须将其清零:

mov dx, 0             ; 清除被除数高16位
mov ax, 8003h         ; 被除数的低16位
mov ex, 100h          ; 除数
div ex                ; AX = 0080h, DX = 0003h

# IDIV 有符号数除法

有符号除法与无符号几乎相同,但是在计算前必须对被除数进行符号扩展。

指令 全称 说明
cbw convert byte to word 将AL的符号位扩展到AH
cwd convert word to doubleword 将AX的符号位扩展到DX
cdq convert doubleword to quadword 将EAX的符号位扩展到EDX
.data
wordval sword -101
 
.code
main PROC
    nop
    mov dx, 0
    mov ax, wordval
    cwd;   DX:AX=FFFF:FF9B,这样的才能保证被除数是负101,否则会被解释为正65435。
    mov bx, 2
    idiv bx
    invoke ExitProcess,0
main ENDP

# 逻辑

常见逻辑指令 and、or、xor、not、test(测试计算数据)等等

# 逻辑指令

格式:

指令 格式
AND AND 操作数1,操作数2
OR OR 操作数1,操作数2
XOR XOR 操作数1,操作数2
TEST TEST 操作数1,操作数2
NOT NOT 操作数1

# AND 指令

实现按位与运算

1、将寄存器清空

AND     AH, 00H     ; 高位AH清空

2、检查奇偶数

AND     AL, 01H     ; 与上 0000 0001

格式示例:

and 【寄存器or内存】【寄存器or内存or立即数】 ✅
and 【内存】【内存】 ❌
运算结果会修改对应的标志:
CF,OF,PF,SF和ZF标志

以下程序说明了这一点-

section .text
   global _start            
        
_start:                     
   mov   ax,   6h           ;设置数据6(二进制 0110 ),下面开始判断它的奇偶
   and   ax, 1              ;通过 0001 & 0110 = 0,此时标记位 ZF=0
   jz    even_f             ; jz: ZF 等于 0 成立,跳转 偶数过程 even_f
   ;jnz   odd_f               jnz: ZF 不等于 0 成立,跳转 奇数过程 odd_f
   jmp   exit               ; jmp 无条件跳转到 exit

even_f:  ;偶数打印
  
   mov   eax, 4             
   mov   ebx, 1             
   mov   ecx, even_msg      
   mov   edx, len1          
   int   0x80               

odd_f:  ;奇数打印
   
   mov   eax, 4             
   mov   ebx, 1             
   mov   ecx, odd_msg       
   mov   edx, len2          
   int   0x80

exit: ;退出
   
   mov   eax,1             
   int   0x80              

section   .data
even_msg  db  '是偶数!' 
len1  equ  $ - even_msg 
   
odd_msg db  '是奇数!'    
len2  equ  $ - odd_msg

跳转相关的标志位: 【⚠️ 关于标志位详细介绍后面篇章有介绍】

11 10 9 8 7 6 5 4 3 2 1 0
OF DF IF TF SF ZF AF PF CF
溢 出 方向 中断 调试 符 号 非零 未 用 辅 助 未 用 奇 偶 未 用 进 位

部分跳转指令和标记位关系:

程序控制类 无条件转移 JMP 不影响标志位
条 件 转 移 单个 标志 位 JS/JNS SF=1/0,则转移到目的地址
JZ/ JNZ ZF=1/0,则转移到目的地址
JP/JNP PF=1/0,则转移到目的地址
JB/JNB CF=1/0,则转移到目的地址
JO/JNO OF=1/0,则转移到目的地址
若干 标志 位的 逻辑 组合 JA 两个无符号数比较,A>B

# OR 指令

OR 指令用于通过执行按位或运算来支持逻辑表达式。

例如

4 or 3 结果 = 7

 mov    al, 4             ;0100
 mov    bl, 3             ;0011
 or     al, bl            ;0100 | 0011 = 0111 
 add    al, byte '0'      ;格式化

# XOR 指令

异或运算:位不同为1,相同为0

例如,

4 xor 3 结果 = 7

 mov    al, 4             ;0100
 mov    bl, 3             ;0011
 or     al, bl            ;0100 xor 0011 = 0111 
 add    al, byte '0'      ;格式化

清零操作:

将操作数与自身进行XOR会将操作数更改为0。这用于清除寄存器。

XOR     EAX, EAX

# TEST 指令

TEST 指令与AND运算的工作原理相同,但与AND指令不同的是,它不会更改第一个操作数。

因此,可以使用test测试某个逻辑运算的结果:

AND    AL, 01H ;会修改AL的值

TEST    AL, 01H ;不会修改AL的值

# NOT 指令

NOT 指令实现按位NOT运算。NOT操作将操作数中的位取反。操作数可以在寄存器中,也可以在存储器中。

NOT 是单操作,接受一个参数

例如,

 mov    al, 4             ;0100
 mov    bl, 3             ;0011
 or     al, bl            ;al = 0100 | 0011 = 0111 
 not    al 			          ;al = 0000 0111 取反 = 1111 1000

# 条件

前面示例已经介绍过一些条件指令,比如 jmp 无条件跳转

# 条件

条件令可以更改程序中的控制流,分为有条件和无条件:

无条件跳转 - JMP 就是最常见的,类似 go to

有条件跳转 - 通过条件判断是否或者如何处理控制流向

# 无条件跳转

语法格式

JMP     label

JMP指令:直接跳转到指定的过程,不做任何判断

_start:                     
   ...
   jmp   exit               ; jmp 无条件跳转到 exit

exit: ;退出
   
   mov   eax,1             
   int   0x80 

# 条件跳转

如果在条件跳转中满足某些指定条件,则控制流将转移到目标指令。

指令 描述 标志测试
JE/JZ 跳转等于或跳转零 ZF
JNE/JNZ 跳转不等于或跳转不为零 ZF
JG/JNLE 跳转大于或跳转不小于/等于 OF,SF,ZF
JGE/JNL 跳转大于/等于或不小于跳转 OF,SF
JL/JNGE 跳转小于或不大于/等于 OF,SF
JLE/JNG 跳少/等于或跳不大于 OF,SF,ZF

以下是对用于逻辑运算的无符号数据使用的条件跳转指令-

指令 描述 标志测试
JE/JZ 跳转等于或跳转零 ZF
JNE/JNZ 跳转不等于或跳转不为零 ZF
JA/JNBE 跳转向上或不低于/等于 CF,ZF
JAE/JNB 高于/等于或不低于 CF
JB/JNAE 跳到以下或跳到不高于/等于 CF
JBE/JNA 跳到下面/等于或不跳到上方 AF,CF

以下条件跳转指令有特殊用途,并检查标志的值-

指令 描述 标志测试
JXCZ 如果CX为零则跳转 没有
JC 如果携带则跳 CF
JNC 如果不携带则跳转 CF
JO 溢出时跳转 OF
JNO 如果没有溢出则跳转 OF
JP/JPE 跳校验或偶校验 PF
JNP/JPO 跳转无奇偶校验或跳转奇偶校验 PF
JS 跳跃符号(负值) SF
JNS 跳转无符号(正值) SF

根据 CX、ECX 寄存器的值跳转:

JCXZ(CX 为 0 则跳转)、JECXZ(ECX 为 0 则跳转);

# 示例

CMP 比较是否相同的指令,通过相减等于0表示相同,否则不相同。

CMP     AL, BL  ;比较他们是否相同,相同 ZF=0
JE      equal		;使用je:判断ZF标记等于0跳转

CMP     AL, BH
JNE     not_equal   ;使用jne:判断ZF标记不等于0跳转

not_equal: 
...

equal: 
...

# 过程

可以理解为c 语言的函数

前面示例 jmp、je等条件指令 跳转的就是 过程或子例程

过程的语法:

proc_name:				 ;过程名称
   procedure body  ;过程主体
   ...
   ret							;过程的结束由return语句指示

示例:

section .text
   global _start      
        
_start:    
   jmp name_1
   
 name_2:
   mov  edx,4           
   mov  ecx, names+8     
   mov  ebx,1          
   mov  eax,4          
   int  0x80  
   jmp exit

 name_1:
   mov  edx,4           
   mov  ecx, names+4     
   mov  ebx,1          
   mov  eax,4          
   int  0x80      
   jmp name_2
   
exit:
   mov  eax,1           
   int  0x80            

section .data
global names
names:
DW  'Lucy', 'Andy', 'Pete'

# 循环

c语法中 while 等能很方便实现循环功能

在汇编中可以通过loop 和jmp 等实现

# 循环 loop

loop指令的格式是:loop 标号

通常我们用loop指令来实现循环功能,cx中存放循环次数。

CPU执行loop指令的时候,要进行两步操作:

1、(cx)=(cx)-1; 2、判断CX中的值,不为零则转至标号处执行程序,如果为零则向下执行。

示例:

循环判断5次,每次都对ax 加一,最后cx=0时循环结束,

	mov ax,5
	mov cx,5
loop_1:		
	inc ax
			
	loop loop_1

1、标号:loop_1

在汇编语言中,标号代表一个地址,程序中有一个标号loop_1。它实际上标识了一个 地址,

这个地址处有一条指令:inc ax

2、loop loop_1:

CPU执行loop s的时候,要进行两步操作:

① (cx)=(cx)-1;(loop 自动计算的)

②判断cx中的值,不为0则转至标号s所标识的地址处执行,为零则执行下一条指令。

3、在这段程序中第一次执行loop loop_1前,cx=5,ax=5

第一次执行后,cx=4,ax=6

最后一次 cx=0,ax=10

4、用CX和loop指令相配合实现循环功能的3个要点:

1、在 CX中存放循环次数; 2、loop指令中的标号所标识地址要在前面; 3、要循环执行的程序段,要写在标号和loop指令的中间

# 例子

从9到0打印:

section .text
   global _start      
        
_start:               
   mov ecx,10				;index 起始值
   mov eax, '9'			;起始值
        
loop_dec:
   mov [num], eax
   mov eax, 4
   mov ebx, 1
   push ecx
        
   mov ecx, num        
   mov edx, 1        
   int 0x80
        
   mov eax, [num]
   sub eax, '0'
   dec eax					;递减
   add eax, '0'
   pop ecx
   loop loop_dec		;循环,每次自动对 ecx 自减,直到0结束循环
        
   mov eax,1             
   int 0x80              
section .bss
num resb 1

结果:

9876543210

# ASCII数值

当数字显示在屏幕上或从键盘输入时,它们是ASCII形式存储的,也就是 输入‘1’ 实际是 49

比如:

mov  eax,'0'

此时,eax 真实数据并非十进制数据0,而是 0的 ASCII 值 48 或者 30 .

所以计算的时候,ASCII 十进制 的话 要先 - 48 得到真实的0

下面观察一组0-9数据表:

Bin(二进制) Oct(八进制) Dec(ASCII 十进制) Hex(ASCII 十六进制) Dec(十进制数值) 解释
0011 0000 060 48 0x30 0 字符0
0011 0001 061 49 0x31 1 字符1
0011 0010 062 50 0x32 2 字符2
0011 0011 063 51 0x33 3 字符3
0011 0100 064 52 0x34 4 字符4
0011 0101 065 53 0x35 5 字符5
0011 0110 066 54 0x36 6 字符6
0011 0111 067 55 0x37 7 字符7
0011 1000 070 56 0x38 8 字符8
0011 1001 071 57 0x39 9 字符9

# ASCII数值操作

假设输入的数值16进制结果 以ASCII 十进制 数理解如下操作:

1、最终输出eax结果:d

mov  eax,'2'	;eax = 50
mov  ebx,'2'	;ebx = 50
add  eax, ebx	;eax = 50 + 50 = 100 

;此时打印eax的值不是4,打印的是 ASCII 100 对应的值d

2、最终输出eax结果:菱形符号控制符号,不能正常输出

mov  eax,2	;eax = 2
mov  ebx,2	;ebx = 2
add  eax, ebx	;eax = 2 + 2 = 4 

;此时打印eax的值不是4,打印的是 ASCII 4 对应的值菱形符号控制符号

3、最终输出eax结果:4

mov  eax,'2'	;eax = 50
mov  ebx,2	;ebx = 2
add  eax, ebx	;eax = 50 + 2 = 52 

;此时打印eax的值是 ASCII 52 对应的值4

4、最终输出eax结果:4

mov  eax,'2'	;eax = 50
sub  eax,'0'	;eax = 50 - 48 = 2

mov  ebx,'2'	;ebx = 50
sub  ebx,'0'	;ebx = 50 - 48 = 2

add  eax, ebx	;eax = 2 + 2 = 4
add  eax, '0' ;eax = 48 + 4 = 52 

;此时打印eax的值是 ASCII 52 对应的值4

下面从网络上收集了ASCII 0 -159 对应的数据

ASCII码大致由三部分组成:

# 1、ASCII 可打印字符对照表

数字 32–126 分配给了能在键盘上找到的字符,当您查看或打印文档时就会出现。注:十进制32代表空格 ,十进制数字 127 代表 DELETE 命令。下面是ASCII码和相应数字的对照表

ASCII 码 字符 ASCII 码 字符 ASCII 码 字符 ASCII 码 字符
十进位 十六进位 十进位 十六进位 十进位 十六进位 十进位 十六进位
032 20 056 38 8 080 50 P 104 68 h
033 21 ! 057 39 9 081 51 Q 105 69 i
034 22 " 058 3A : 082 52 R 106 6A j
035 23 # 059 3B ; 083 53 S 107 6B k
036 24 $ 060 3C < 084 54 T 108 6C l
037 25 % 061 3D = 085 55 U 109 6D m
038 26 & 062 3E > 086 56 V 110 6E n
039 27 ' 063 3F ? 087 57 W 111 6F o
040 28 ( 064 40 @ 088 58 X 112 70 p
041 29 ) 065 41 A 089 59 Y 113 71 q
042 2A * 066 42 B 090 5A Z 114 72 r
043 2B + 067 43 C 091 5B [ 115 73 s
044 2C , 068 44 D 092 5C \ 116 74 t
045 2D - 069 45 E 093 5D ] 117 75 u
046 2E . 070 46 F 094 5E ^ 118 76 v
047 2F / 071 47 G 095 5F _ 119 77 w
048 30 0 072 48 H 096 60 ` 120 78 x
049 31 1 073 49 I 097 61 a 121 79 y
050 32 2 074 4A J 098 62 b 122 7A z
051 33 3 075 4B K 099 63 c 123 7B {
052 34 4 076 4C L 100 64 d 124 7C |
053 35 5 077 4D M 101 65 e 125 7D }
054 36 6 078 4E N 102 66 f 126 7E ~
055 37 7 079 4F O 103 67 g 127 7F DEL

# 2、ASCII 非打印控制字符

ASCII 表上的数字 0–31 分配给了控制字符,用于控制像打印机等一些外围设备。例如,12 代表换页/新页功能。此命令指示打印机跳到下一页的开头。

进制 十六进制 字符 十进制 十六进制 字符
0 00 空 16 10 数据链路转意
1 01 头标开始 17 11 设备控制 1
2 02 正文开始 18 12 设备控制 2
3 03 正文结束 19 13 设备控制 3
4 04 传输结束 20 14 设备控制 4
5 05 查询 21 15 反确认
6 06 确认 22 16 同步空闲
7 07 震铃 23 17 传输块结束
8 08 backspace 24 18 取消
9 09 水平制表符 25 19 媒体结束
10 0A 换行/新行 26 1A 替换
11 0B 竖直制表符 27 1B 转意
12 0C 换页/新页 28 1C 文件分隔符
13 0D 回车 29 1D 组分隔符
14 0E 移出 30 1E 记录分隔符
15 0F 移入 31 1F 单元分隔符

# 3、扩展 ASCII 可打印字符

十进制 十六进制 字符 十进制 十六进制 字符
32 20 space 80 50 P
33 21 ! 81 51 Q
34 22 " 82 52 R
35 23 # 83 53 S
36 24 $ 84 54 T
37 25 % 85 55 U
38 26 & 86 56 V
39 27 ' 87 57 w
40 28 ( 88 58 X
41 29 ) 89 59 Y
42 2A * 90 5A Z
43 2B + 91 5B [
44 2C , 92 5C /
45 2D - 93 5D ]
46 2E . 94 5E ^
47 2F / 95 5F _
48 30 0 96 60 `
49 31 1 97 61 a
50 32 2 98 62 b
51 33 3 99 63 c
52 34 4 100 64 d
53 35 5 101 65 e
54 36 6 102 66 f
55 37 7 103 67 g
56 38 8 104 68 h
57 39 9 105 69 i
58 3A : 106 6A j
59 3B ; 107 6B k
60 3C < 108 6C l
61 3D = 109 6D m
62 3E > 110 6E n
63 3F ? 111 6F o
64 40 @ 112 70 p
65 41 A 113 71 q
66 42 B 114 72 r
67 43 C 115 73 s
68 44 D 116 74 t
69 45 E 117 75 u
70 46 F 118 76 v
71 47 G 119 77 w
72 48 H 120 78 x
73 49 I 121 79 y
74 4A J 122 7A z
75 4B K 123 7B {
76 4C L 124 7C
77 4D M 125 7D }
78 4E N 126 7E ~
79 4F O 127 7F DEL

# 数值数据

section .text
   global _start        ;must be declared for using gcc
        
_start:                 ;tell linker entry point
   mov  eax,'5'
   sub     eax, '0'
        
   mov  ebx, '6'
   sub     ebx, '0'
   add  eax, ebx
   add  eax, '0'
        
   mov  [sum], eax
   mov  ecx,msg 
   mov  edx, len
   mov  ebx,1            ;file descriptor (stdout)
   mov  eax,4            ;system call number (sys_write)
   int  0x80             ;call kernel
        
   mov  ecx,sum
   mov  edx, 1
   mov  ebx,1            ;file descriptor (stdout)
   mov  eax,4            ;system call number (sys_write)
   int  0x80             ;call kernel
        
   mov  eax,1            ;system call number (sys_exit)
   int  0x80             ;call kernel
        
section .data
msg db "The sum is:", 0xA,0xD 
len equ $ - msg   
segment .bss
sum resb 1

结果:(显然结果不是我们想看到的)

The sum is:
;

TODO:

1、每次运算都要对每个寄存器处理有一定消耗

2、能计算个位,但是没法进行进位,比如不能正确表示出11

# ASCII表示

在ASCII表示中,十进制数字存储为ASCII字符字符串,运算需要使用正确的汇编指令进行处理

输入‘0’,结果其实是

30H

输入十进制值0123存储为:

30 		31     32      33H

其中,30H是0的ASCII值,31H是1的ASCII值,依此类推。

所以要输出一个11需要这样:

section .text
   global _start        
        
_start:   
   mov     ax, 3131H	 ;设置 ax值 11
   
   mov     [res], ax

   mov  edx,2           ;长度2
   mov  ecx,res         ;赋值
   mov  ebx,1           
   mov  eax,4           
   int  0x80            
        
   mov  eax,1           
   int  0x80            
                      
section .bss
res resb 2  

所以在本篇汇编中是不能完全直接使用 add 、sub直接加减的

1、处理ASCII运算的指令

ASCII运算的指令 (未压缩) 解释
AAA(⚠️ 都是单指令,后不接操作数) 加法后ASCII调整
AAS 减法后的ASCII调整
AAM 乘法后ASCII调整
AAD 除法前ASCII调整

2、处理对象

这些指令处理的是 ax(eax 等),不需要指定,类似 ‘隐含寻址’

3、使用方式

;加法运算
add     ax, '6'
aaa

;减法运算
sub     ax, '6'
aas

示例:

计算 5+6=11

section .text
   global _start        
        
_start:                 ;tell linker entry point
   ;初始化操作,清空ax中的 高地位数据
   sub     al, ah
   sub     ah, ah
   
   ;运算
   mov     ax, '5'			;ax = 00 35H
   
   add     ax, '6'			;ax = 00 35H + 00 36H = 00 6BH
   aaa		;调整ax中值(不是其它寄存器)
   ;⚠️ 使用add后接着使用aaa表示格式化,将上面加法后ASCII值进行调整
   ;调整前 00 6BH ,处理后 ax = 01 01H,高低位各一个1表示 11的值
   
   or      al, 30h			;ax = 01 31H
   or      ah, 30h			;ax = 31 31H
   
   mov     [res], ax		;res = 11

   mov  edx,2           ;11占两个字节,如果设置1个字节的长度的话返回的是1不是11
   mov  ecx,res         ;写入结果
   mov  ebx,1           
   mov  eax,4           
   int  0x80            
        
   mov  eax,1           
   int  0x80           
                      
section .bss
res resb 2  

结果:

11

# 压缩

对于一个十进制数1234

1、1234 的 ASCII十六进制表示:

31 32 33 34 H

对应的未压缩十进制表示:

01 02 03 04

2、所以前面上一个示例中 aaa 的结果就是未压缩十进制表示

01 01

3、对应压缩后十进制表示:

12 34

从上可以看到,压缩后可以减少一半的存储空间。

压缩后数据运算指令:(⚠️ 不支持 )

ASCII运算的指令 (压缩) 解释
DAA 加法后的十进制调整
DAS 减后的十进制调整
乘法 不支持
除法 不支持

对比加法运算后 aaa(未压缩) 和 daa (压缩)结果

1、aaa

mov     ax, '5'			;ax = 00 35H
add     ax, '6'			;ax = 00 6BH
aaa 								;ax = 01 01H

2、daa

mov     ax, '5'			;ax = 00 35H
add     ax, '6'			;ax = 00 6BH
daa 								;ax = 00 11H

示例:

section .text
   global _start        ;must be declared for using gcc

_start:                 ;tell linker entry point

   mov     esi, 4       ;pointing to the rightmost digit
   mov     ecx, 5       ;num of digits
   clc				; 清除标志位 CF的值,即CF=0
add_num:  
   mov  al, [num1 + esi]
   adc  al, [num2 + esi]
   aaa
   pushf
   or   al, 30h
   popf
        
   mov  [sum + esi], al
   dec  esi
   num  add_num
        
   mov  edx,len         ;message length
   mov  ecx,msg         ;message to write
   mov  ebx,1           ;file descriptor (stdout)
   mov  eax,4           ;system call number (sys_write)
   int  0x80            ;call kernel
        
   mov  edx,5           ;message length
   mov  ecx,sum         ;message to write
   mov  ebx,1           ;file descriptor (stdout)
   mov  eax,4           ;system call number (sys_write)
   int  0x80            ;call kernel
        
   mov  eax,1           ;system call number (sys_exit)
   int  0x80            ;call kernel

section .data
msg db 'The Sum is:',0xa        
len equ $ - msg                 
num1 db '12345'
num2 db '23456'
sum db '     '

尝试一下 (opens new window)

编译并执行上述代码后,将产生以下结果-

The Sum is:
35801

# 标志位

前面在介绍逻辑章节的时候使用到了标志位,此节详细介绍每一个标志位(intel 16、32位)。

CPU内部的寄存器中有一种特殊的寄存器,被称为标志寄存器,具有以下三种作用,:

(1)用来存储相关指令的某些执行结果;

(2)用来为CPU执行相关指令提供行为依据;

(3)用来控制CPU的相关工作方式。

# 状态字(PSW)

程序运行中一些记录信息,根据CPU不同,存储状态信息的寄存器也不同,有16位,32位,64位等等

# 标志寄存器(flag)

标志,顾名思义就是记录一些状态当前的信息,方便做判断,

flag寄存器是按位起作用的,每一位都有专门的含义,8086 CPU的flag寄存器的结构:

位(高-低) flag(计算结果标志位) 含义(计算后的标记)
15
14
13
12
11 OF(有符号运算是否有溢出) if (结果溢出) { of=1} else { of=0 }
10 DF(递增递减方向) si、di递增 df=0,si、di递减df=1
9 IF(中断允许) 允许并响应外部中断时if=1,反之屏蔽
8 TF(调试) tf=1表示处理器每次只执行一条指令,调试模式
7 SF(非负) if (结果负数) { sf=1} else { sf=0 }
6 ZF(非0) if (计算结果为0) { zf=1} else { zf=0 }
5
4 AF(辅助进位) af=1表示运算过程中最后四位有进位
3
2 PF(奇偶) if (计算结果为偶数) { pf=1} else { pf=0 }
1
0 CF(无符号运算是否有借位) 结果显示不下产生了借位,cf=1

其中1、3、5、12、13、14、15位在8086 CPU中没有使用

# OF标志

有符号运算溢出标志位。

记录了有符号运算的结果是否发生了溢出,如果发生溢出OF=1,如果没有OF=0;

# DF标志

方向标志位。

在串处理指令中,每次操作后,如果DF=0,si、di递增,如果DF=1,si、di递减;

DF的值是由程序员进行设定的 cld命令是将DF设置为0,std命令是将DF设置为1;

# IF标志

中断允许标志位。

它用来控制是否允许接收外部中断请求。

若IF=1,能响应外部中断,反之则屏蔽外部中断;

# TF标志

调试标志位。

当TF=1时,处理器每次只执行一条指令,即单步执行;

# SF标志

flag的第7位是SF,符号标志位。

它记录相关指令执行后,其结果是否为负。如果结果为负,sf=1;如果非负,sf=0。

计算机中通常用补码来表示有符号数据(负数)。

运算后可以通过它来得知结果的正负。

mov ax,1
sub ax,2
;1-2<0,此时sf=1

# ZF标志

flag的第6位是ZF,零标志位

它记录相关指令执行后,结果为0,ZF=1(记录下是0这样的肯定信息),结果不为0,ZF=0(表示结果非0)。

mov ax,5
sub ax,5
;指令执行后,结果为0,则ZF=1

mov ax,5
and ax,0
;指令执行后,结果为0,则ZF=1

mov ax,5
sub ax,1
;指令执行后,结果为4,不为0,则ZF=0

mov ax,1
or ax,0
;指令执行后,结果为1,不为0,则ZF=0

# AF标志

辅助进位标志位。

运算过程中看最后四位,不论长度为多少。最后四位向前有进位或者借位,AF=1,否则AF=0;

# PF标志

flag的第2位是PF,奇偶标志位。

记录指令执行后结果所有的二进制位中1的个数。为偶数,PF=1;为奇数,PF=0。

mov al,1
add al,2
;执行结果为00000011B,有2个1,偶数,则PF=1

mov al,1
or al,0
;执行后结果为00000001B,有1个1,奇数,则PF=1

# CF标志

flag的第0位是CF,无符号运算进位标志位。

是无符号运算记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。

对于位数为N的无符号数,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位,假想存在第N位是相对于最高有效位的更高位。

mov al,98h
add al,al      ;98H + 98H = 01 30H; al 只有两个字节,所以执行后(al)=30H,cf=1,cf记录了从最高有效位向更高位的进位值
add al,al      ;30H + 30H = 60H; 执行后(al)=60H,cf=0,cf记录了从最高有效位向更高位的进位值

sub al,al      ;执行后(al)=0,cf=0,cf记录了向更高位的借位值

# 1.1 abc指令

adc是带进位加法指令,利用了CF位上记录的进位值。

格式:adc 操作对象1,操作对象2

功能:操作对象1=操作对象1+操作对象2+CF。

mov ax,2
mov bx,1
sub bx,ax
adc ax,1
执行后 (ax)=4,相当于计算(ax)+1+CF=2+1+1+4

mov ax,1
add ax,ax
adc ax,3
执行后(ax)=5,相当于执行(ax)+3+CF=2+3+0=5

mov al,98H
add al,al
adx al,3
执行后 (al)=34H,相当于执行(ax)+3+CF=30H+3+1=34H

由adc指令前面的指令,决定在执行adc指令的时候加上的CF的值的含义。也就是说关键在于所加上的CF值是被什么指令设置的。

1、进位值

如果是被add指令设置的,那么它的含义就是进位值。

加法运算分两步进行:

①低位相加

②高位相加加上低位相加产生的进位值。

CPU提供adc指令的目的,就是来进行加法的第二步运算的。adc指令和add指令相配合就可以对更大的数据进行加法运算。

2、借位值

如果CF的值是被sub指令设置的,那么它的含义就是借位值;

编程:计算1EF000H+201000H,结果存放在AX(高16位)和BX(低16位)中。

mov ax,001EH
mov bx,0F000H
add bx,1000H
adc ax,0020H
;编程:1EF0001000H+2010001EF0H,结果存放在AX(高16位)、BX(次16位)中和cx(低16位)。

计算分三步进行:

(1)先将低16位相加,完成后,CF中记录本次相加的进位值。
(2)再将次高16位和CF(来自低16位的进位值)相加,完成后,CF中记录本次相加的进位值。
(3)最后高16位和CF(来自次高16位的进位值)相加,完成后,CF中记录本次相加的进位值。
mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
add bx,1000H
adc ax,0020H

编程:对两个128位数据进行相加

assume cs:code,ds:data
    data segment
            db 16 dup(88H)
            db 16 dup(11H)
    data ends
    code segment
    start:
            mov ax,data
            mov ds,ax
            mov si,0
            mov di,16
            mov cx,8
            call add128
            mov ax,4C00H
            int 21H
    add128:
            push ax
            push cx
            push si
            push di
            sub ax,ax;将CF设置为0
        s:
            mov ax,[si]
            adc ax,[di]
            mov [si],ax
            inc si;不能用add si,2代替
            inc si;因为会影响cf位
            inc di;而loop和inc不会影响
            inc di
            loop s
             pop di
             pop si
             pop cx
             pop ax
             ret
    code ends
end start

# 1.2 sbb指令

sbb是带借位减法指令,利用了CF位上记录的借位值。

格式:sbb 操作对象1,操作对象2

功能:操作对象1=操作对象1-操作对象2-CF

利用sbb指令我们可以对任意大的数据进行减法运算。

sbb和adc是基于同样的思想设计的两条指令,在应用思路上sbb和adc类似。

编程:计算003E1000H-00202000H,结果放在ax,bx中

mov bx,1000H
mov ax,003EH
sub bx,2000H
sbb ax,0020H

# 1.3 cmp指令

cmp是比较指令,功能上相当于减法指令,只是不保存结果。

格式:cmp 操作对象1,操作对象2

功能:计算操作对象1-操作对象2但不保存结果,仅仅是根据计算结果对标志寄存器进行设置。

cmp指令运算执行后通过做减法将对标志寄存器产生影响,其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。

cmp ax,ax
;执行后结果为0,ZF=1,PF=1,SF=0,CF=0,OF=0

mov ax,8
mov bx,3
cmp ax,bx
;执行后ax、bx的值不变,ZF=0,PF=1,SF=0,CF=0,OF=0
;通过cmp指令执行后,相关标志位的值就可以看出比较的结果。 
;指令"cmp ax,bx”的逻辑含义是比较ax和bx中的值

判断结果大小

指令"cmp ax,bx”的逻辑含义是比较ax和bx中的值,如果执行后:

cmp ax,bx 比较后的标志状态 含义
ZF=1 ax = bx
ZF=0 ax != bx
CF=1 ax < bx
CF=0 ax >= bx
CF=0 & ZF=0 ax > bx
CF=1 & ZF=1 ax <= bx

判断结果正负

⚠️ sf记录的是ah中的8位二进制信息所表示的数据的正负, 而cmp并不修改寄存器的值,单纯地考察SF的值不可能知道结果的正负

OF 0: 没有溢出,1:溢出
SF 1:表示负数,0 正数
OF=0 & SF=1 (ah)<(bh)
OF=0 & SF=0 (ah)≥(bh)
OF=1 & SF=1 (ah)>(bh)
OF=1 & SF=0 (ah)<(bh)

1、无溢出时通过sf标记可判断结果的正负

如果SF=1或SF=0,OF=0,逻辑上真正结果的正负=实际结果的正负。

of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;

若sf=1,实际结果为负,所以逻辑上真正的结果为负,所以(ah)<(bh)

若sf=0,实际结果非负,所以逻辑上真正的结果非负,所以(ah)≥(bh)

2、有溢出时通过sf标记 判断结果的**正负****

如果SF=1或SF=0,OF=1,逻辑上真正结果的负正≠实际结果的正负。

of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;

若sf=1,实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,简单分析一下,就可以看出,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正,则说明(ah)>(bh)。

若sf=0,实际结果非负,而of=1说明有溢出,则结果非0,所以,实际结果为正。实际结果为正,而又有溢出,这说明是由于溢出导致了导致了实际结果非负,简单分析一下就可以看出,如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负,则说明(ah)<(bh)。

# 1.4 条件转移指令

条件转移指令需根据比较指令、并配合标志综合使用,

比如与cmp相配使用,根据cmp指令的比较结果(cmp指令执行后相关标志位的值)进行工作的指令。

cmp指令可以同时进行两种比较,无符号数比较和有符号数比较,所以根据cmp指令的比较结果进行转移的指令也分为两种:

1、无符号比较

根据无符号数的比较结果进行转移的条件转移指令,它们检测ZF、CF的值;

2、有符号比较

根据有符号数的比较结果进行转移的条件转移指令,它们检测SF、OF、ZF的值。

跳转指令 跳转条件 标志位
JE = ZF=1
JNE != ZF=0
JB < CF=1
JNB >= CF=0
JA > CF=0 & ZF=0
JNA <= CF=1 & ZF=1
 ;编程:如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)。

  cmp ah,bh
  je s                       ;ZF=1则跳转
  add ah,bh
  jmp short ok
    s: add ah,bh
   ok: ret 

 ;je检测的是ZF的位置,不管je前面是什么指令,只要CPU执行je指令时,ZF=1那么就发生转移。
		mov ax,0
    mov ax,0
    je s
    inc ax
s:
    inc ax
    
  ;执行后(ax)=1,add ax,0使得ZF=1,所以je指令将进行转移。 

编程:统计data段中数值为8的字节的个数,用ax保存统计结果。

方案一

assume cs:code
    data segment
            db 8,11,8,1,8,5,63,38
    data ends
    code segment
    start:
            mov ax,data
            mov ds,ax
            mov bx,0;ds:bx指向第一个字节
            mov ax,0;初始化累加器
            mov cx,0
        s:
            cmp byte ptr [bx],8;和8进行比较
            jne next;如果不相等转到next,继续循环
            inc ax;如果相等就计数值加1
        next:
            inc bx
            loop s;执行后:(ax)=3
            mov ax,4c00h
            int 21h
    code ends
end segment

方案二

assume cs:code
    data segment
            db 8,11,8,1,8,5,63,38
    data ends
    code segment
    start:
            mov ax,data
            mov ds,ax
            mov bx,0;ds:bx指向第一个字节
            mov ax,0;初始化累加器
            mov cx,0
        s:
            cmp byte ptr [bx],8;和8进行比较
            je ok;如果不相等转到ok,继续循环
            jmp short next;如果不想等就转到next,继续循环
        ok:
            inc ax;如果相等就计数值加1
        next:
            inc bx
            loop s;执行后:(ax)=3
            mov ax,4c00h
            int 21h
    code ends
end segment 

编程:统计data段中数值大于8的字节的个数,用ax保存统计结果。

assume cs:code
    data segment
            db 8,11,8,1,8,5,63,38
    data ends
    code segment
    start:
            mov ax,data
            mov ds,ax
            mov bx,0;ds:bx指向第一个字节
            mov ax,0;初始化累加器
            mov cx,0
        s:
            cmp byte ptr [bx],8;和8进行比较
            jna next;如果大于8转到next,继续循环
            inc ax;如果大于就计数值加1
        next:
            inc bx
            loop s;执行后:(ax)=3
            mov ax,4c00h
            int 21h
    code ends
end segment

# 1.5 串传送指令

1、DF标志:方向标志位

在串处理指令中,控制每次操作后si(一般指向原始偏移地址)、di(一般指向目标偏移地址)的增减。

DF=0:每次操作后si、di递增;

DF=1:每次操作后so、di递减。

2、movsb串传送指令 movsb(mov string byte)串传送指令

以字节为单位传送

格式:movsb

执行movsb相当于以下操作:

(1)((es)*16+(di))=((ds)*16+(si))

(2)如果DF=0,则(si)=(si)+1,(di)=(di)+1;
	
	如果DF=1,则(si)=(si)-1,(di)=(di)-1。
	movsb功能:将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器DF位的值将si和di递增1或递减1。

2、movsw串传送指令

以字为单位传送。

将ds:si指向的内存单元中的字送入es:di中,然后根据标志寄存器DF位的值将si和di递增2或递减2。

movsb和movsw进行的是串传送操作中的一个步骤,一般和rep配合使用,格式:rep movsb,rep的作用是根据cx 的值,重复执行后面的串传送指令。由于每执行一次movsb指令si和di都会递增或递减指向后一个单元或前个单元,则rep movsb就可以循环实现(cx)个字符的传送。

使用串传送指令进行数据的传送,需要提供:

1、传送的原始位置;

2、传送的目的位置;

3、传送的长度;

4、传送的方向。

由于flag的DF位决定着串传送指令执行后,si和di改变的方向,8086CPU提供两条指令对DF位进行设置:

(1)cld指令:将标志寄存器的DF位设置为0;

(2)std指令:将标志寄存器的DF位设置为1。

# 1.6 pushf和popf指令

pushf的功能是将标志寄存器的值压栈,

popf是从栈中弹出数据,送入标志寄存器中。

pushf和popf为直接访问标志寄存器提供了一种方法。

下面的程序执行后ax的值是多少?

mov ax,0
push ax
popf
mov ax,0fff0h
add ax,0010h
pushf
pop ax
and al,11000101b
and ah,00001000b

# 1.7 Debug

标志寄存器在Debug中的表示

标志 值为1的标记 值为。的标记
OF OV NV
SF NG PL
ZF ZR NZ
PF PE PO
CF CY NC
DF DN UP

# 字符串

可以使用 section .data 显式存储字符串,

或者 section .bss 段定义字符串变量再赋值

# 字符串

我们可以使用表示位置计数器当前值的$位置计数器符号来显式存储字符串长度

msg  db  'Hello, world!',0xa ;我们常见的字符
len  equ  $ - msg            ;长度
;$指向字符串变量msg的最后一个字符之后的字节。因此,$ - msg给出字符串的长度

# 字符串指令

对于16位地址,使用SI和DI寄存器,对于32位地址,使用ESI和EDI寄存器。

下表提供了各种版本的字符串指令和假定的操作数空间:

基本指令 操作的寄存器 字节运算 字运算 双字运算 说明
MOVS ES:DI,DS:SI MOVSB MOVSW MOVSD 该指令将1字节,字或双字数据从存储位置移到另一个位置
LODS DS:SI LODSB LODSW LODSD 该指令从存储器加载。如果操作数是一个字节,则将其加载到AL寄存器中;如果操作数是一个字,则将其加载到AX寄存器中,并将双字加载到EAX寄存器中
STOS ES:DI,AX STOSB STOS STOSD 该指令将数据从寄存器(AL,AX或EAX)存储到存储器
CMPS DS:SI,ES:DI CMPSB CMPSW CMPSD 该指令比较内存中的两个数据项。数据可以是字节大小,字或双字
SCAS ES:DI,AX SCASB SCASW SCASD 该指令将寄存器(AL,AX或EAX)的内容与内存中项目的内容进行比较

下面介绍一些会用到的指令

# 标志位重置指令

CLD 将方向标志位DF清零
STD 将方向标志位DF置1

# REP重复执行指令

1、REP(Repeat)汇编指令是一种重复执行指令的控制指令,它通常和其他指令组合使用,用于在处理字符串或数组时进行重复操作。REP指令可以和多个其他指令搭配使用,如MOV、STOS等。其语法如下:

rep instruction ;比如 rep MOVSB

其中,instruction是要重复执行的指令。

2、REP指令会将ECX寄存器中的值作为计数器,循环执行instruction指定的操作。

每次循环都会将ECX减1,直到ECX的值为0为止(类似loop)

3、对于不同的操作码,REP有三种形式:

REPE、REP Repeat while Equal 比较结果相同时继续执行,不相同退出循环
REPNE、REPN Repeat while Not Equal 比较结果不相同时继续执行,相同退出循环
REPZ Repeat while Zero 结果 =0 时继续执行,结果 !=0 退出循环

# MOVS指令

MOVS指令用于将数据项(字节,字或双字)从源字符串SI 到目标字符串DI :

section .text
   global _start 
        
_start:              
   mov  ecx, len       ;s1长度
   mov  esi, s1        ;将s1的字符串送入 si 寄存器中
   mov  edi, s2        ;开辟 s2空间并指定到 di 寄存器
   cld					       ;方向标志位DF清零
   rep  movsb	         ;源字符串SI 送到目标字符串DI
        
   mov  edx,20          ;s2长度
   mov  ecx,s2          ;s2信息
   mov  ebx,1           ;stdout
   mov  eax,4           ;sys_write
   int  0x80            ;call 
        
   mov  eax,1           ;system call number (sys_exit)
   int  0x80            ;call kernel
        
section .data
s1 db 'Hello, world!'
len equ $-s1

section  .bss
s2 resb 20              

结果:

Hello, world!

# LODS指令

该指令从存储器加载。如果操作数是一个字节,则将其加载到AL寄存器中;如果操作数是一个字,则将其加载到AX寄存器中,并将双字加载到EAX寄存器中

示例:

将secret加密显示,加密方式:单字节增加2位

section .text
   global _start       
        
_start:                 
   mov    ecx, 6      ;secret字符串长度,同时也是loop循环的次数
   mov    esi, s1      	;将s1的字符串送入 si 寄存器中
   mov    edi, s2      	;开辟 s2空间并指定到 di 寄存器
      
loop_encrypt: 			;加密(循环6次)
   lodsb				;将si中的'secret'加载到AL(AX或EAX)寄存器
   add al, 02		;al内容偏移2(混淆)
   stosb 				;该指令将数据从寄存器AL(AX或EAX)存储到存储器
   loop    loop_encrypt          
   
   cld					       ;方向标志位DF清零
   rep  movsb	         ;源字符串SI 送到目标字符串DI
        
   mov     edx,20        
   mov     ecx,s2       
   mov     ebx,1        
   mov     eax,4        
   int     0x80         
        
   mov     eax,1        
   int     0x80         
        
section .data
s1 db 'secret key'
len equ $-s1

section .bss
s2 resb 10             

结果:

ugetgv

同样如果想要将密文ugetgv解密,转换片段:

encrypt_code: 			;解密(循环6次)
   lodsb				;将si中的密文'ugetgv'加载到AL(AX或EAX)寄存器
   sub al, 02		;al内容偏移2(还原)
   stosb 				;该指令将数据从寄存器AL(AX或EAX)存储到存储器
   loop    encrypt_code          

结果还原为'secret'

# STOS指令

STOS指令将数据项从AL(对于字节-STOSB),AX(对于字-STOSW)或EAX(对于双字-STOSD)到目标字符串,该目标字符串由内存中的ES:DI指向。

上面示例已经使用了stosb

将大写字符串转换为其小写值转换片段:

   
lower_case:
   lodsb
   or      al, 20h
   stosb
   loop    lower_case    
   
   cld
   rep  movsb
 

# REP、REPE 重复指令

REP前缀在字符串指令(例如-REP MOVSB)之前设置时,会根据放置在CX寄存器中的计数器使该指令重复。REP执行该指令,将CX减1,然后检查CX是否为零。重复指令处理,直到CX为零为止。

方向标志(DF)确定操作的方向。

- 使用CLD(清除方向标志,DF = 0)使操作从左到右。
- 使用STD(设置方向标志,DF = 1)使操作从右到左。

REP前缀也有以下变化:

- REP:这是无条件的重复。重复该操作,直到CX为零为止。
- REPE或REPZ:这是有条件的重复。当零标志指示等于/零时,它将重复操作。当ZF表示不等于零或CX为零时,它将停止。
- REPNE或REPNZ:这也是有条件的重复。当零标志指示不等于/零时,它将重复操作。当ZF指示等于/零或CX减为零时,它将停止。

# CMPS指令

CMPS指令:

比较两个字符串。该指令比较DS:SI和ES:DI寄存器所指向的两个数据项,即一个字节,一个字或一个双字,并相应地设置标志。

REPE:

比较结果相同时继续执行,不相同退出循环,

1、循环次数同loop使用cx寄存器值

2、不同的是,cx递减到0前满足退出循环条件的时候就结束了,不会继续循环,比loop 损耗小

JECXZ:

根据 CX、ECX 寄存器的值跳转: JCXZ(CX 为 0 则跳转)、JECXZ(ECX 为 0 则跳转);

section .text
   global _start           
        
_start: 
   mov esi, s1
   mov edi, s2
   mov ecx, lens2
   
   cld
   repe  cmpsb              ;重复执行cmpsb命令,直到 cx=0或者 比较结果不相同 结束
   jecxz  equal             ;CX 为 0 则跳转
	 jmp nequal

nequal: 
   ;不相同时逻辑
   mov eax, 4
   mov ebx, 1
   mov ecx, msg_neq
   mov edx, len_neq
   int 80h
   jmp exit
        
equal: ;相同时逻辑
   mov eax, 4
   mov ebx, 1
   mov ecx, msg_eq
   mov edx, len_eq
   int 80h
   jmp exit
        
exit: ;退出
   mov eax, 1
   mov ebx, 0
   int 80h
        
section .data
s1 db 'Hello, world!',0      ;源字符
lens1 equ $-s1

s2 db 'Hello, world!', 0     ;比较字符
lens2 equ $-s2

msg_eq db '相同的字符串!', 0xa
len_eq  equ $-msg_eq

msg_neq db '不相同的字符串!'
len_neq equ $-msg_neq

结果

相同的字符串!

# SCAS指令

SCAS指令用于搜索字符串中的特定字符或一组字符。要搜索的数据项应该在AL(对于SCASB),AX(对于SCASW)或EAX(对于SCASD)寄存器中。要搜索的字符串应在内存中,并由ES:DI(或EDI)寄存器指向。查看以下程序以了解概念-

section .text
   global _start        ;must be declared for using gcc
        
_start:                 ;tell linker entry point

   mov ecx,len
   mov edi,my_string
   mov al , 'e'
   cld
   repne scasb
   je found ; when found
   ; If not not then the following code
        
   mov eax,4
   mov ebx,1
   mov ecx,msg_notfound
   mov edx,len_notfound
   int 80h
   jmp exit
        
found:
   mov eax,4
   mov ebx,1
   mov ecx,msg_found
   mov edx,len_found
   int 80h
        
exit:
   mov eax,1
   mov ebx,0
   int 80h
        
section .data
my_string db 'hello world', 0
len equ $-my_string  

msg_found db '包含该字符串!', 0xa
len_found equ $-msg_found

msg_notfound db '不包含该字符串!'
len_notfound equ $-msg_notfound   

结果-:

包含该字符串!

# 数组

C++语法中数组示例:

int marks[6] = {34,  45,  56,  67,  75, 89};

# 数组

我们已经讨论过,汇编程序的数据定义指令用于为变量分配存储空间。变量也可以用一些特定的值初始化。初始化值可以以十六进制,十进制或二进制形式指定。例如,我们可以通过以下两种方式之一来定义单词变量“months”-

MONTHS  DW      12			;十进制表示12月
MONTHS  DW      0CH			;十六进制表示12月
MONTHS  DW      0110B		;二进制表示12月

1、连续数据定义

分配连续内存空间,初始化值0

List1   DW  0, 0 , 0 , 0 , 0 , 0 

定义一维数字数组(连续的相同类型)。

List1 DW  34,  45,  56,  67,  75, 89
;分配了2x6 = 12个字节的连续存储空间

定义相同内容时可以使用 times

List1 TIMES 6 DW 0

定义6个8

List1 TIMES 6 DW 8

2、访问数据

第一个数字的符号地址为List1,第二个数字的符号地址为List1 + 2,依此类推。

比如访问 56

[List1 + 4]

访问第二个名称:

section .text
   global _start        ;must be declared for linker (ld)
        
_start:    
   
   mov  edx,4           ;message length
   mov  ecx, names+4      ;message to write
   mov  ebx,1           ;file descriptor (stdout)
   mov  eax,4           ;system call number (sys_write)
   int  0x80      
   
   mov  eax,1           ;system call number (sys_exit)
   int  0x80            ;call kernel

section .data
global names
names:
DW  'Lucy', 'Andy', 'Pete'

结果

Andy

# 堆栈

内存结构:

堆栈是内存中类似数组的连续内存数据结构,通常会固定一段存储空间。

可以在其中存储数据并从称为“堆栈顶部”的位置删除数据。

入栈:

需要存储的数据被“推送”到堆栈中

出栈:

要检索的数据从堆栈中“弹出”。

LIFO:

堆栈是一种LIFO(后进先出)数据结构,即首先存储的数据最后被检索。

PUSH和POP:

汇编语言为堆栈操作提供了两条指令:PUSH和POP。

语法如下:

PUSH    operand
POP     address/register

堆栈段中保留的内存空间用于实现堆栈。寄存器SS和ESP(或SP)用于实现堆栈。

SS栈顶指针寄存器:

指向堆栈段的开头,也就是起始地址,通常是最高位开始

SP(或ESP)偏移量:

SS起始地址上,再偏移SP的偏移量,就是栈顶的位置,也就是最后存储的一条数据位置。

比如:

从1-5将数据存储到堆栈(0x01-0x09)的存储空间上:

堆栈示意地址 插入的数据 位置
0x09 1 SS:存放栈顶的段地址(最高位为起始地址)
0x08 2
0x07 3
0x06 4
0x05 5 SS: SP:sp偏移量结合的位置为栈顶(最后数据5的位置)
0x04
0x03
0x02
0x01

pop的顺序依次是:5,4,3,2,1 ,和push是相反的

- 只能将字或双字保存到堆栈中,而不是字节。
- 堆栈朝反方向增长,即朝着较低的存储器地址增长
- 堆栈的顶部指向插入堆栈中的最后一个项目。它指向插入的最后一个字的低字节。

使用场景,比如:

MOV     AX, 1
MOV     BX, 2

; 将AX和BX寄存器内容保存在堆栈中
PUSH    AX
PUSH    BX

;将寄存器用着其他用途
MOV     AX, 3
MOV     BX, 4
...

;使用完之后恢复寄存器原始值,最后push的BX要最先pop才行。
POP     BX ;BX=2
POP     AX ;AX=1

以下程序显示整个ASCII字符集。主程序调用一个名为display的过程,该过程显示ASCII字符集。

section .text
   global _start        
        
_start: 
	 ;call 指令表示执行一段代码,需要ret 配合返回并继续执行后续代码
   call init_count ;从'5' 开始递增打印20个ASCII字符
   call next  ;循环打印
   call exit
   
init_count:     
   mov    ecx, 20 
	 ret
next:
   push    ecx
   call display  ;调用显示代码块
        
   pop     ecx  
   mov  dx, [achar]
   cmp  byte [achar], 0dh
   inc  byte [achar]
   loop    next
   ret
   
 display:
   mov     eax, 4
   mov     ebx, 1
   mov     ecx, achar
   mov     edx, 1
   int     80h  
   ret
   
exit:
   mov  eax,1           ;system call number (sys_exit)
   int  0x80            ;call kernel

section .data
achar db '5' 

结果:

56789:;<=>?@ABCDEFGH

# 递归

递归过程是一个调用自身的过程。

上一个示例中我们使用到了 call 指令; call 指令表示执行一段代码,需要ret 配合返回并继续执行后续代码

# 递归

示例:直接递归(方法内调用自己)

3阶乘c示意代码:

int a=3,b=3;

fun mul_fun {
	if (b <= 1) return; // 结束
	b--;
	a = a*b;
	mul_fun();
}

汇编示例:

section .text
   global _start      
        
_start:               

   mov bx, 3
	 mov ax, 3
   call  mul_fun

mul_fun: ;阶乘递归
    cmp   bl, 1
    JLE	  p_total ;<= 1时结束,跳转到打印方法
    dec bl
    mul bl ;ax = al * bl
    call mul_fun
        
p_total: ;打印
   add   ax, 30h
   mov  [fact], ax
   
   mov    edx,1            ;message length
   mov    ecx,fact       ;message to write
   mov    ebx,1          ;file descriptor (stdout)
   mov    eax,4          ;system call number (sys_write)
   int    0x80           ;call kernel
    
   mov    eax,1          ;system call number (sys_exit)
   int    0x80           ;call kernel

section .bss
fact resb 1

结果:

6
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式