六、汇编实战
打印:hello world
在屏幕上输出字符
mov dl,'a' ; 将要打印的字符放到dl中
mov ah, 02h ; 设置显示字符的功能号
int 21h ; 调用DOS中断,打印字符
在屏幕上输出字符串
mov ah,09h ;设置显示字符串的功能号
int 21H ;调用BIOS中断
字符串的结束符:$
在一个段中定义要打印的字符串,并在字符串结尾写上'$',当输出字符的中断遇到"$"时,就会停止打印字符
data segment
str db 'Hello World', 0dh, 0ah, '$' ;在data段定义要打印的字符串
;其中0dh和0ah为回车和换行符;'$'为字符串结束符
data ends
打印字符串
data segment
str db 'Hello World', 0dh, 0ah, '$' ;在data段定义要打印的字符串
;其中0dh和0ah为回车和换行符;'$'为字符串结束符
data ends
code segment
;关联code段和data段的段地址寄存器
assume cs:code, ds:data
start:
;关联data段
mov ax,data
mov ds,ax
mov dx,offset str ;设置dx指向str字符串的起始位置
mov ah,09h ;设置显示字符串的功能号
int 21H ;调用BIOS中断
;退出程序
mov ah,4CH
int 21H
code ends
end start
运行后效果如下:
打印uint16_t范围的数字
C语言版
注意:Uart0_PutChar的作用是串口发送一个字节的数据,类似于打印一个字符串的功能
/**
* 函 数:次方函数(内部使用)
* 返 回 值:返回值等于X的Y次方
*/
uint16_t Serial_Pow(uint16_t X, uint16_t Y)
{
uint16_t Result = 1; //设置结果初值为1
while (Y--) //执行Y次
{
Result *= X; //将X累乘到结果
}
return Result;
}
/**
* 函 数:串口发送数字
* 参 数:Number 要发送的数字,范围:0~65535
* 参 数:Length 要发送数字的长度,范围:0~5
* 返 回 值:无
*/
void print_uint16_t(uint16_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++) //根据数字长度遍历数字的每一位
{
Uart0_PutChar(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次调用Uart0_PutChar发送每位数字
}
}
转换为汇编
data segment
;定义数据空间
str db 5 dup(0), 0dh, 0ah,'$' ;用于在屏幕上显示的字符串
var dw 0 ;在程序运行时可供使用的变量
Len dw 0 ;待打印的数字长度
Number dw 0 ;待打印的数字
Result dw 0 ;数字取整所要除的数
data ends
stack segment
;定义栈空间
dw 10 dup(0)
stack ends
code segment
;关联code段和data段的段地址寄存器
assume cs:code, ds:data, ss:stack
;定义可以在屏幕上打印uint16范围内的数字的函数
print_uint16:
;先清空str中的数据
mov si,offset str
mov cx,5
mov ax,32
Clear:
mov [si],ax ;32对应的ascll是空格
inc si
loop Clear
mov cx,[Len] ;要打印的数字长度
s:
push cx ;保存循环计数器
dec cx
mov [Result], 1 ;初始化除数
;次方函数
Serial_Pow:
cmp cx,0
je s2
mov ax,10 ;计算16位的乘法,其中一个乘数要在ax中
mul [Result] ;计算Result乘以10
mov [Result],ax ;16位的乘法低16位在ax中,将其移到Result中,因为Result不会超过16位,所以不用管高16位
loop Serial_Pow
s2:
pop cx ;恢复循环计数器
;计算16位的除法,被除数必须放在在dx和ax中,除数在Result中
mov dx,0
mov ax,[Number] ;将32位被除数放入ax和bx中
div [Result] ;对Number取整,取整的结果在ax中,余数在dx中
;ax中存放的是取整后的结果,将dx清零后可再次进行除法运算
mov dx,0 ;清除余数,准备对取整后的结果再进行取余
mov bx,10
div bx ;对十进制数取余,余数在dx中
add dl,30h ;将余数转换为ASCII码
;将数字高位放到左边,低位放到右边
mov ax,[Len]
mov [var],ax
sub [var],cx
mov bx,[var]
mov [str+bx],dl
loop s
;打印字符串
mov dx,offset str ;设置显示字符串的偏移地址
mov ah,09h ;设置显示字符串的功能号
int 21H ;调用BIOS中断
ret
;程序开始,相当于main函数
start:
;关联段与段寄存器
mov ax,data
mov ds,ax
mov ax,stack
mov ss,ax
;相当于给print_uint16函数传参
mov [Number],12345 ;在屏幕上打印12345
mov [Len],5
;调用print_uint16函数
call print_uint16
mov [Number],65535 ;在屏幕上打印65535
mov [Len],5
call print_uint16
mov [Number],777 ;在屏幕上打印777
mov [Len],3
call print_uint16
mov [Number],1 ;在屏幕上打印00001
mov [Len],5
call print_uint16
;退出程序
mov ah,4CH
int 21H
code ends
end start
运行后效果如下:
优化版
该版本不用设置数字长度,只需要将要打印的uint16_t类型的数字放到Number中即可
先看分文件编写后再看这个
; 声明该代码块可以被其他模块访问和调用
public Lib_print_uint16
.model small ; 使用小内存模型,代码段和数据段各64KB
.stack 100h ; 定义堆栈段大小为256字节 (100h = 256)
.data
Number dw 0 ; 定义一个字(16位)变量Number,初始值为0
Result dw 0 ; 定义一个字(16位)变量Result,用于存储数字取整所要除的数
var dw 0 ; 定义一个字(16位)变量var,用于在程序运行时临时存储数据
.code
Lib_print_uint16 proc
; 从栈中获取Number的地址
pop [var] ; 获取返回地址到var中,因为是近距离转移,所以只把偏移地址压入栈中,pop一次就够了
pop [Number] ; 将要打印的数字放到Number中
push [var] ; 将返回地址重新压入栈中
; 保存寄存器状态
push ax
push bx
push cx
push dx
push si
push di
mov bx, 10 ; 计算十进制的位数,用10对待计算的数取整
mov cx, 0 ; 初始化cx寄存器为0,cx将用于计数Number的位数
mov ax, [Number] ; 将Number的值加载到ax寄存器中
cmp ax, 0 ; 比较ax寄存器中的值是否为0
jne Count_Bits ; 如果ax不为0,跳转到Count_Bits标签,开始计数位数
; 如果ax为0,直接打印并退出
mov dl, 30h ; 将字符'0'加载到dl寄存器
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出字符'0'
mov dl, 0Ah ; 将0Ah(换行符)加载到dl寄存器
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出换行符
mov dl, 0Dh ; 将0Dh(回车符)加载到dl寄存器
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出回车符
jmp End_Count ; 跳转到End_Count标签,打印
Count_Bits:
inc cx ; 增加cx寄存器的值,计数器加1
mov dx, 0 ; 清空dx寄存器,用于除法操作
div bx ; 将ax寄存器中的值除以bx寄存器中的值,ax = ax / bx, dx = ax % bx
cmp ax, 0 ; 比较ax寄存器中的值是否为0
jne Count_Bits ; 如果ax不为0,继续循环
; 开始循环打印数字,数字长度已经在cx中
s:
push cx ; 保存循环计数器
dec cx ; 将cx减1,用于计算当前位数
mov [Result], 1 ; 初始化除数Result为1
Serial_Pow:
cmp cx, 0 ; 比较cx和0
je s2 ; 如果cx等于0,跳转到s2
mov ax, 10 ; 将10加载到ax寄存器
mul [Result] ; 计算Result乘以10,结果低16位在ax,高16位在dx
mov [Result], ax ; 将ax中的结果存储到Result中
loop Serial_Pow ; 循环直到cx为0,计算10的幂次
s2:
pop cx ; 恢复循环计数器
; 计算16位的除法,被除数必须放在dx和ax中,除数在Result中
mov dx, 0 ; 清空dx寄存器,准备进行除法运算
mov ax, [Number] ; 将Number的值加载到ax寄存器
div [Result] ; 对Number取整,取整的结果在ax中,余数在dx中
; ax中存放的是取整后的结果,将dx清零后可再次进行除法运算
mov dx, 0 ; 清空dx寄存器,准备进行下一步的除法运算
div bx ; 对十进制数取余,余数在dx中
add dl, 30h ; 将余数转换为ASCII码(数字0的ASCII码是30h)
mov ah, 02h ; 设置显示字符的功能号
int 21h ; 调用DOS中断,打印字符
loop s ; 循环直到cx为0,将数字转换为字符串
mov dl, 0Ah ; 将0Ah(换行符)加载到dl寄存器
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出换行符
mov dl, 0Dh ; 将0Dh(回车符)加载到dl寄存器
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出回车符
End_Count:
; 恢复寄存器的状态
pop di ; 恢复di寄存器
pop si ; 恢复si寄存器
pop dx ; 恢复dx寄存器
pop cx ; 恢复cx寄存器
pop bx ; 恢复bx寄存器
pop ax ; 恢复ax寄存器
ret ; 返回调用处
Lib_print_uint16 endp ; 结束过程定义
end ; 标记程序结束
调用方法
push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16 call Lib_print_uint16 ; 调用Lib_print_uint16函数,该函数负责打印指定长度的16位无符号整数
再次优化版
该版本可以指定进制
对应的c代码:
/*
* 函 数:将uint16_t范围内的整数,转换成任意进制的字符串,并打印
* 参 数:Number 待打印的整数,范围:0~65535
* 参 数:scale 指定进制,范围:0~16
* 返 回 值:无
*/
void print_uint16_t(unsigned int Number, int Scale)
{
char str[16];
int count = 0;
while (Number)
{
char b = Number % Scale;
if ((b += 0x30) > 57)b += 7;
str[count] = b;
Number /= Scale;
count++;
}
while (count)
{
count--;
printf("%c", str[count]);
}
printf("\r\n");
}
; 声明该代码块可以被其他模块访问和调用
public Lib_print_uint16
.model small ; 使用小内存模型,代码段和数据段各64KB
.stack 100h ; 定义堆栈段大小为256字节 (100h = 256)
.data
str db 16 dup(0) ; 存储待打印的字符,初始化为16个0字节的空间
scale dw 0 ; 进制,这里用于设置要转换的数字的进制,默认为0
Number dw 0 ; 定义一个字(16位)变量Number,初始值为0,用于存放要打印的数字
.code ; 代码段开始
Lib_print_uint16 proc ; 定义一个名为Lib_print_uint16的过程
; 从栈中获取Number的地址
pop word ptr [str] ; 将返回地址暂时存在str的前两个字中
pop [Number] ; 将栈顶的值弹出并存储到Number变量中,这个值是要打印的16位无符号整数
pop [scale] ; 将栈顶的值弹出并存储到scale变量中,这个值是要转换的进制
push word ptr [str] ; 将之前弹出的返回地址重新压入栈中,以便正确返回调用点
; 保存寄存器状态,防止过程执行期间修改寄存器值影响到调用方
push ax ; 保存ax寄存器的值
push bx ; 保存bx寄存器的值
push cx ; 保存cx寄存器的值
push dx ; 保存dx寄存器的值
push si ; 保存si寄存器的值
push di ; 保存di寄存器的值
mov ax, [Number] ; 将Number的值加载到ax寄存器中,准备进行除法操作
mov bx, [scale] ; 将scale的值加载到bx寄存器中,这里应该是要转换的进制数(如10进制)
mov si, 0 ; 初始化si为0,作为索引,用于向str中存储字符
mov dx, 0 ; 清空dx寄存器,用于除法操作前的准备。在div指令中,dx:ax会作为被除数
for1:
mov dx, 0 ; 在每次循环开始前,清空dx,确保除法操作的正确性
div bx ; 用ax除以bx(scale),ax存放商,dx存放余数。这里将Number的值转换为字符表示
add dl, 30h ; 将余数转换为ASCII码,30h是字符'0'的ASCII码,加30h后可以得到相应的数字字符
cmp dl, 58 ; 检查dl中的ASCII码是否大于'9'(58h是字符':'的ASCII码,但在这里应该是想检查是否大于'9')
jb No_16 ; 如果dl中的ASCII码不大于'9',跳转到No_16标签处
add dl, 7 ; 如果dl中的ASCII码大于'9',说明是10进制以上的转换,这里将dl加7h是想将字符转换为'A'-'F'(在16进制中)
No_16:
mov str[si], dl ; 将转换后的字符存储到str数组中,si作为索引
inc si ; si索引加1,指向数组的下一个位置
cmp ax, 0 ; 比较商ax是否为0
jne for1 ; 如果商ax不为0,继续循环
for2:
dec si ; 循环结束后,si指向的是数组的结束位置(多加了一次1),所以这里si减1指向数组的最后一个元素
mov dl, str[si] ; 将str数组中si位置的字符加载到dl寄存器中,准备输出
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出dl中的字符
cmp si, 0 ; 比较si是否为0
jne for2 ; 如果si不为0,继续循环,输出str数组中的所有字符
mov dl, 0Ah ; 将0Ah(换行符)加载到dl寄存器中,准备输出换行符
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出换行符
mov dl, 0Dh ; 将0Dh(回车符)加载到dl寄存器中,准备输出回车符
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出回车符
; 恢复寄存器的状态,确保调用方的寄存器未被修改
pop di ; 恢复di寄存器的值
pop si ; 恢复si寄存器的值
pop dx ; 恢复dx寄存器的值
pop cx ; 恢复cx寄存器的值
pop bx ; 恢复bx寄存器的值
pop ax ; 恢复ax寄存器的值
ret ; 返回调用处
Lib_print_uint16 endp ; 结束过程定义
end ; 标记程序结束
调用方法
push [scale] push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16 call Lib_print_uint16 ; 调用Lib_print_uint16函数,该函数负责打印指定长度的16位无符号整数
分文件编写
内存模型
在MASM(Microsoft Macro Assembler)中,内存模型(memory model)定义了程序中代码段(code segment)、数据段(data segment)、堆栈段(stack segment)和附加段(extra segment)的大小和访问方式。
不同的内存模型适用于不同大小的程序和不同的内存管理需求。
-
紧凑型内存模型(.model compact)
-
段的大小
- 代码段(CS): 64KB
- 数据段(DS): 64KB
- 堆栈段(SS): 64KB
- 附加段(ES): 64KB
-
特点
- 代码段和数据段可以共享同一个64KB的地址空间。
- 堆栈段和附加段可以共享同一个64KB的地址空间。
- 使用近指针(near pointers)和远指针(far pointers)。
- 适用于需要在数据段和堆栈段之间共享内存的程序。
-
-
中型内存模型(.model medium)
-
段的大小
- 代码段(CS): 1MB
- 数据段(DS): 64KB
- 堆栈段(SS): 64KB
- 附加段(ES): 64KB
-
特点
- 代码段可以超过64KB,达到1MB。
- 数据段、堆栈段和附加段每个都限制在64KB的范围内。
- 使用远指针(far pointers)。
- 适用于代码量较大但数据量较小的程序。
-
-
大型内存模型(.model large)
-
段的大小
- 代码段(CS): 1MB
- 数据段(DS): 1MB
- 堆栈段(SS): 1MB
- 附加段(ES): 1MB
-
特点
- 所有段都可以达到1MB的大小。
- 使用远指针(far pointers)。
- 适用于大型程序,需要更大的代码和数据段。
- 段寄存器(CS、DS、SS、ES)可以独立指向不同的段。
-
-
巨大内存模型(.model huge)
-
段的大小
- 代码段(CS): 1MB
- 数据段(DS): 1MB
- 堆栈段(SS): 1MB
- 附加段(ES): 1MB
-
特点
- 类似于大型内存模型,但主要用于处理非常大的数据结构。
- 使用远指针(far pointers)。
- 适用于需要处理非常大的数据结构的程序。
-
-
大型紧凑型内存模型(.model large compact)
-
段的大小
- 代码段(CS): 1MB
- 数据段(DS): 64KB
- 堆栈段(SS): 64KB
- 附加段(ES): 64KB
-
特点
- 代码段可以达到1MB。
- 数据段、堆栈段和附加段每个都限制在64KB的范围内。
- 使用远指针(far pointers)。
- 适用于代码量较大但数据量较小的程序,数据段和堆栈段可以共享64KB的地址空间。
-
-
大型紧凑型内存模型(.model large compact)
-
段的大小
- 代码段(CS): 1MB
- 数据段(DS): 64KB
- 堆栈段(SS): 64KB
- 附加段(ES): 64KB
-
特点
- 代码段可以达到1MB。
- 数据段、堆栈段和附加段每个都限制在64KB的范围内。
- 使用远指针(far pointers)。
- 适用于代码量较大但数据量较小的程序,数据段和堆栈段可以共享64KB的地址空间。
-
-
中型紧凑型内存模型(.model medium compact)
-
段的大小
- 代码段(CS): 64KB
- 数据段(DS): 64KB
- 堆栈段(SS): 64KB
- 附加段(ES): 64KB
-
特点
- 代码段和数据段可以共享同一个64KB的地址空间。
- 堆栈段和附加段可以共享同一个64KB的地址空间。
- 使用近指针(near pointers)和远指针(far pointers)。
- 适用于需要在数据段和堆栈段之间共享内存的程序。
-
基于内存模型定义的段
示例
.model small ;使用小内存模型
.stack 256 ;定义栈段的大小为256个字节,默认填充0
.data ;定义数据段
d1 db 10 dup(0)
.code ;定义代码段
start:
;将ds段寄存器指向data段
mov ax,@data
mov ds,ax
;退出程序
mov ah,4ch
int 21h
end start
等价于:
;关联段与段寄存器 assume cs:code, ds:data, ss:stack stack segment ;定义栈段 dw 256 dup(0) ;定义栈段的大小为256个字节,用0填充 stack ends data segment ;定义数据段 d1 db 10 dup(0) data ends code segment ;定义代码段 start: ;将ds段寄存器指向data段 mov ax,data mov ds,ax ;将ss段寄存器指向stack段 mov ax,stack mov ss,ax ;退出程序 mov ah,4ch int 21h data ends end start
使用.data和data segment的区别
当只有一个文件时没有区别,当多个文件定义数据段时,多个文件使用.data定义的数据段是连续的,而使用data segment则可能会在不同的内存中,每调用其他文件中的模块都需要重新设置ds段寄存器
使用外部代码块的伪指令
PROC和ENDP:定义一个过程
-
作用:将重复使用的一段代码块拿出来作为一个子程序。 在汇编语言中,通常用术语过程(procedure)来指代子程序。在其他语言中,子程序也被称为方法或函数。
-
过程的概念: 过程可以非正式地定义为:由返回语句结束的已命名的语句块。过程用 PROC 和 ENDP 伪指令来定义,并且必须为其分配一个名字(有效标识符) 。感觉也可以叫他函数
-
语法:
-
近距离调用(不跨段)
过程名 proc ... ;具体的功能代码 ret 过程名 endp
-
远距离调用(跨段)
过程名 proc far ... ;具体的功能代码 retf ;注意返回符的不同 过程名 endp
-
-
注意:
-
定义在过程中的标号只有过程内可见。
标号类似于变量,过程类似函数,函数中定义的变量为局部变量,只有函数内可访问。标号之于过程与变量之于函数是一样的道理
-
如果想让过程中定义的标号全局可见,只需要多写一个冒号,如下
过程名 proc 标号1: ;该标号只有过程内部可见 标号2:: ;该标号全局可见 ret 过程名 endp
-
PUBLIC:声明一个标号可被其他模块调用
作用: 汇编伪指令,用于说明程序模块中的某个标号是可以被其他程序模块调用的。
格式:public 标号
EXTRN:声明一个标号是使用其他模块中的
作用: 汇编伪指令,用于说明程序模块中用到的标号是其他程序模块的
格式:
-
extrn 标号:类型
其中类型包括: near、far、byte、word、dword等。
-
near: 近调用,过程地址在同一个代码段内。
-
far: 远调用,过程地址可以位于不同的代码段内。
-
byte: 声明一个字节(8位)变量。
-
word: 声明一个字(16位)变量。
-
dword: 声明一个双字(32位)变量。
-
...
-
-
extrn 标号
- 在 small 和 compact 内存模型中,默认类型为 near。
- 在 large 和 huge 内存模型中,默认类型为 far。
- 对于数据变量(如
byte
,word
,dword
),如果不指定类型,默认为word
。
示例:将打印uin16_t范围的数字独立到另一个文件中
将上面打印uint16_t范围内的数字的代码放到单独的一个文件中,实现模块化编程
在同级目录中创建main.asm
和print.asm
两个文件,代码如下:
print.asm
; 声明该代码块可以被其他模块访问和调用
public Lib_print_uint16
.model small ; 使用小内存模型,代码段和数据段各64KB
.stack 100h ; 定义堆栈段大小为256字节 (100h = 256)
.data
str db 16 dup(0) ; 存储待打印的字符,初始化为16个0字节的空间
scale dw 0 ; 进制,这里用于设置要转换的数字的进制,默认为0
Number dw 0 ; 定义一个字(16位)变量Number,初始值为0,用于存放要打印的数字
.code ; 代码段开始
Lib_print_uint16 proc ; 定义一个名为Lib_print_uint16的过程
; 从栈中获取Number的地址
pop word ptr [str] ; 将返回地址暂时存在str的前两个字中
pop [Number] ; 将栈顶的值弹出并存储到Number变量中,这个值是要打印的16位无符号整数
pop [scale] ; 将栈顶的值弹出并存储到scale变量中,这个值是要转换的进制
push word ptr [str] ; 将之前弹出的返回地址重新压入栈中,以便正确返回调用点
; 保存寄存器状态,防止过程执行期间修改寄存器值影响到调用方
push ax ; 保存ax寄存器的值
push bx ; 保存bx寄存器的值
push cx ; 保存cx寄存器的值
push dx ; 保存dx寄存器的值
push si ; 保存si寄存器的值
push di ; 保存di寄存器的值
mov ax, [Number] ; 将Number的值加载到ax寄存器中,准备进行除法操作
mov bx, [scale] ; 将scale的值加载到bx寄存器中,这里应该是要转换的进制数(如10进制)
mov si, 0 ; 初始化si为0,作为索引,用于向str中存储字符
mov dx, 0 ; 清空dx寄存器,用于除法操作前的准备。在div指令中,dx:ax会作为被除数
for1:
mov dx, 0 ; 在每次循环开始前,清空dx,确保除法操作的正确性
div bx ; 用ax除以bx(scale),ax存放商,dx存放余数。这里将Number的值转换为字符表示
add dl, 30h ; 将余数转换为ASCII码,30h是字符'0'的ASCII码,加30h后可以得到相应的数字字符
cmp dl, 58 ; 检查dl中的ASCII码是否大于'9'(58h是字符':'的ASCII码,但在这里应该是想检查是否大于'9')
jb No_16 ; 如果dl中的ASCII码不大于'9',跳转到No_16标签处
add dl, 7 ; 如果dl中的ASCII码大于'9',说明是10进制以上的转换,这里将dl加7h是想将字符转换为'A'-'F'(在16进制中)
No_16:
mov str[si], dl ; 将转换后的字符存储到str数组中,si作为索引
inc si ; si索引加1,指向数组的下一个位置
cmp ax, 0 ; 比较商ax是否为0
jne for1 ; 如果商ax不为0,继续循环
for2:
dec si ; 循环结束后,si指向的是数组的结束位置(多加了一次1),所以这里si减1指向数组的最后一个元素
mov dl, str[si] ; 将str数组中si位置的字符加载到dl寄存器中,准备输出
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出dl中的字符
cmp si, 0 ; 比较si是否为0
jne for2 ; 如果si不为0,继续循环,输出str数组中的所有字符
mov dl, 0Ah ; 将0Ah(换行符)加载到dl寄存器中,准备输出换行符
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出换行符
mov dl, 0Dh ; 将0Dh(回车符)加载到dl寄存器中,准备输出回车符
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出回车符
; 恢复寄存器的状态,确保调用方的寄存器未被修改
pop di ; 恢复di寄存器的值
pop si ; 恢复si寄存器的值
pop dx ; 恢复dx寄存器的值
pop cx ; 恢复cx寄存器的值
pop bx ; 恢复bx寄存器的值
pop ax ; 恢复ax寄存器的值
ret ; 返回调用处
Lib_print_uint16 endp ; 结束过程定义
end ; 标记程序结束
main.asm
.model small ; 定义程序的模型为small模型,代码段和数据段各占64KB
; 声明外部函数Lib_print_uint16,表示该函数在其他地方定义
extrn Lib_print_uint16:proc
.stack 100h ; 定义堆栈段的大小为256字节(100h = 256)
.data ; 数据段开始
arr dw 0, 1, 10 dup(0)
Number dw 0 ; 用于存储要打印的数字
scale dw 10 ; 指定要打印的数的进制,默认10进制
.code ; 代码段开始
print_uint16: ; 定义一个子程序print_uint16
push [scale]
push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16
call Lib_print_uint16 ; 调用Lib_print_uint16函数,该函数负责打印指定长度的16位无符号整数
ret ; 返回
start: ; 定义程序的入口点
mov ax, @data ; 将数据段的起始地址加载到AX寄存器中
mov ds, ax ; 将AX寄存器中的地址复制到DS寄存器中,DS是数据段寄存器
mov ax, 43981 ; 将DI寄存器指向的数组元素的值加载到AX寄存器中
mov [Number], ax ; 将AX寄存器中的值存储到变量Number中
mov [scale],16 ; 转成16进制后打印
call print_uint16 ; 调用子程序print_uint16打印Number
mov [scale],10 ; 转成10进制后打印
call print_uint16 ; 调用子程序print_uint16打印Number
mov [scale],8 ; 转成8进制后打印
call print_uint16 ; 调用子程序print_uint16打印Number
mov [scale],2 ; 转成2进制后打印
call print_uint16 ; 调用子程序print_uint16打印Number
; 程序结束
mov ah, 4Ch ; 设置DOS程序结束功能号,4C00h表示程序正常结束
int 21H ; 调用DOS中断21H,执行程序结束功能
end start ; 指定程序的结束点,并且程序从start标签开始执行
编译、链接、运行
-
编译
masm main.asm masm print.asm
-
链接(注意,两个文件必须放一起进行链接)
link main.obj print.obj
-
运行
main.exe
-
运行结果如下:
转换字母的大小写
通过观察ASLLC码可知,字母大写与小写只有第五位不一样,如下
当第五位为0时,该字母是大写,为1时为小写
所以在转换大小写时,只需要改变他们的第五位即可
示例:
data segment
str db 'Hello World', 0dh, 0ah, '$' ;在data段定义要打印的字符串
;其中0dh和0ah为回车和换行符;'$'为字符串结束符
data ends
code segment
;关联code段和data段的段地址寄存器
assume cs:code, ds:data
;定义print子程序,打印str字符串
print:
mov dx,offset str ;设置dx指向str字符串的起始位置
mov ah,09h ;设置显示字符串的功能号
int 21H ;调用BIOS中断
ret
start:
;关联data段
mov ax,data
mov ds,ax
;调用print子程序打印字符串,原始字符串
call print
;将str字符串中的所有字母转换为大写字母
mov si,offset str ;设置si指向str字符串的起始位置
mov cx,11 ;设置cx为字符串长度
s1:
mov al,[si] ;将si指向的字符赋值给al
and al,0dfh ;将al中的字符中的第5位(大写字母的第5位为0)置为0
mov [si],al ;将al中的字符赋值给si指向的位置
inc si ;指向下一个字符的地址
loop s1 ;循环执行s1,直到将替换完所有字符
;调用print子程序打印字符串,转换为大写的字符串
call print
;将str字符串中的所有字母转换为小写字母
mov si,offset str ;设置si指向str字符串的起始位置
mov cx,11 ;设置cx为字符串长度
s2:
mov al,[si] ;将si指向的字符赋值给al
or al,20h ;将al中的字符中的第5位(小写字母的第5位为1)置为1
mov [si],al ;将al中的字符赋值给si指向的位置
inc si ;指向下一个字符的地址
loop s2 ;循环执行s2,直到将替换完所有字符
;调用print子程序打印字符串,转换为小写的字符串
call print
;退出程序
mov ah,4CH
int 21H
code ends
end start
运行后结果如下
求数组的最大和最小值
这里的print.asm使用的是没有优化的版本,详细可见“分文件编写”
求最大值
data segment
;定义数组,寻找该数组的最大值
arr db 2,1,9,4,6,7,8,5,3
;定义字符串,显示数组的最大值,?代表占位符,等会替换为数组的最大值
str db 'sum of arr is: ', ?, 0dh, 0ah,'$'
data ends
code segment
;关联code段和data段的段地址寄存器
assume cs:code, ds:data
;定义print子程序,打印str字符串
print:
mov dx,offset str ;设置显示字符串的偏移地址
mov ah,09h ;设置显示字符串的功能号
int 21H ;调用BIOS中断
ret
start:
;关联data段
mov ax,data
mov ds,ax
mov si,offset arr ;设置数组的偏移地址
mov ah,[si] ;获取数组第一个元素,作为最大值
mov cx,9 ;设置循环次数,也就是数组的长度
sum:
mov al,[si] ;获取数组元素
cmp ah,al ;比较最大值和当前元素
ja big ;如果假设的最大值小于当前元素
mov ah,al ;则更新最大值,否则跳过
big:
inc si ;指向下一个元素,准备下一次比较
loop sum
add ah,30h ;将最大值转换为ASCII码
mov [str+15],ah ;替换字符串的占位符为最大值
call print ;调用print子程序打印最大值
;退出程序
mov ah,4CH
int 21H
code ends
end start
运行结果如下:
求最小值同理,ja指令改为jb指令即可
数组求和
.model small ; 定义程序的模型为small模型,代码段和数据段各占64KB
; 声明外部函数Lib_print_uint16,表示该函数在其他地方定义
extrn Lib_print_uint16:proc
.stack 100h ; 定义堆栈段的大小为256字节(100h = 256)
.data ; 数据段开始
arr db 1, 2, 3, 4, 5, 6, 7, 8, 9, 0
Number dw 0 ; 用于存储要打印的数字
.code ; 代码段开始
print_uint16: ; 定义一个子程序print_uint16
push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16
call Lib_print_uint16 ; 调用Lib_print_uint16函数,该函数负责打印指定长度的16位无符号整数
ret ; 返回
start: ; 定义程序的入口点
mov ax, @data ; 将数据段的起始地址加载到AX寄存器中
mov ds, ax ; 将AX寄存器中的地址复制到DS寄存器中,DS是数据段寄存器
mov di, offset arr ; 将数组arr的起始地址加载到DI寄存器中
mov ax, 0 ; 将AX寄存器清零,准备用于累加数组元素的值
mov cx, 10 ; 将CX寄存器设置为10,表示要累加的数组元素个数
for:
add al, [di] ; 将DI寄存器指向的数组元素的值加到AL寄存器中
adc ah, 0 ; 将AL寄存器的进位加到AH寄存器中
inc di ; 将DI寄存器增加1,指向数组的下一个元素
loop for ; 循环次数减1,如果不为零则跳转到for标签处继续执行
mov [Number],ax ; 设置Number
call print_uint16 ; 调用子程序打印
; 程序结束
mov ah, 4Ch ; 设置DOS程序结束功能号,4C00h表示程序正常结束
int 21H ; 调用DOS中断21H,执行程序结束功能
end start ; 指定程序的结束点,并且程序从start标签开始执行
运行结果如下:
计算斐波那契数列
.model small ; 定义程序的模型为small模型,代码段和数据段各占64KB
; 声明外部函数Lib_print_uint16,表示该函数在其他地方定义
extrn Lib_print_uint16:proc
.stack 100h ; 定义堆栈段的大小为256字节(100h = 256)
.data ; 数据段开始
arr dw 0, 1, 10 dup(0)
Number dw 0 ; 用于存储要打印的数字
.code ; 代码段开始
print_uint16: ; 定义一个子程序print_uint16
push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16
call Lib_print_uint16 ; 调用Lib_print_uint16函数,该函数负责打印指定长度的16位无符号整数
ret ; 返回
start: ; 定义程序的入口点
mov ax, @data ; 将数据段的起始地址加载到AX寄存器中
mov ds, ax ; 将AX寄存器中的地址复制到DS寄存器中,DS是数据段寄存器
; 计算斐波那契数列
mov di, offset arr ; 将数组arr的起始地址加载到DI寄存器中
add di, 4 ; 将DI寄存器增加4,指向数组的第三个元素(假设前两个元素已初始化)
mov cx, 10 ; 将CX寄存器设置为10,表示要计算的斐波那契数列的个数
s:
mov ax, 0 ; 将AX寄存器清零,准备用于计算斐波那契数列的当前值
add ax, [di-2] ; 将DI寄存器指向的前一个元素的值加到AX寄存器中
add ax, [di-4] ; 将DI寄存器指向的再前一个元素的值加到AX寄存器中
mov [di], ax ; 将计算得到的斐波那契数列的当前值存储到数组arr的当前元素中
add di, 2 ; 将DI寄存器增加2,指向数组的下一个元素
loop s ; 循环次数减1,如果不为零则跳转到s标签处继续执行
mov di, offset arr ; 将数组arr的起始地址重新加载到DI寄存器中
mov cx, 12 ; 将CX寄存器设置为12,表示要打印的数组元素个数
s1:
mov ax, [di] ; 将DI寄存器指向的数组元素的值加载到AX寄存器中
add di, 2 ; 将DI寄存器增加2,指向数组的下一个元素
mov [Number], ax ; 将AX寄存器中的值存储到变量Number中
call print_uint16 ; 调用子程序print_uint16打印Number
loop s1 ; 循环次数减1,如果不为零则跳转到s1标签处继续执行
; 程序结束
mov ah, 4Ch ; 设置DOS程序结束功能号,4C00h表示程序正常结束
int 21H ; 调用DOS中断21H,执行程序结束功能
end start ; 指定程序的结束点,并且程序从start标签开始执行
运行结果:
获取输入的字符串数量
获取键盘输入的字符串
-
在数据段定义输入缓冲区,用来存放输入的数据
.data ; 数据段开始 buffer db 255, 0, 255 dup(0) ;定义输入缓冲区
缓冲区第一个字节表示最大可接收的字符数量
第二个字节表示实际接收到的字符数量
第三个字节开始存储接收到的字符
-
将输入缓冲区的地址加载到dx中
lea dx,buffer ;等价于:mov dx,offset buffer
-
调用输入中断
mov ah,0ah ; 设置ah为0ah,这是DOS中断21h中用于输入字符串的服务号 int 21h ; 调用DOS中断21h,执行字符串输入操作
-
在输入完成后记得打印回车换行,避免程序后面要打印字符时与输入的字符重叠
mov dl,0ah ; 将0ah(换行符)加载到dl寄存器 mov ah,2 ; 设置ah为2,是DOS中断21h中用于输出字符的服务号 int 21h ; 调用DOS中断21h,输出换行符 mov dl,0dh ; 将0dh(回车符)加载到dl寄存器 mov ah,2 ; 设置ah为2,是DOS中断21h中用于输出字符的服务号 int 21h ; 调用DOS中断21h,输出回车符
完整示例如下
.model small ; 定义程序的模型为small模型,代码段和数据段各占64KB
; 声明外部函数Lib_print_uint16,表示该函数在其他地方定义
extrn Lib_print_uint16:proc
.stack 100h ; 定义堆栈段的大小为256字节(100h = 256)
.data ; 数据段开始
buffer db 255, 0, 255 dup(0) ;定义输入缓冲区
Number dw 0 ; 用于存储要打印的数字
.code ; 代码段开始
print_uint16: ; 定义一个子程序print_uint16
push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16
call Lib_print_uint16 ; 调用Lib_print_uint16函数,该函数负责打印指定长度的16位无符号整数
ret ; 返回
start: ; 定义程序的入口点
mov ax, @data ; 将数据段的起始地址加载到AX寄存器中
mov ds, ax ; 将AX寄存器中的地址复制到DS寄存器中,DS是数据段寄存器
lea dx,buffer
mov ah,0ah
int 21h
mov dl,0ah ; 将0ah(换行符)加载到dl寄存器
mov ah,2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出换行符
mov dl,0dh ; 将0dh(回车符)加载到dl寄存器
mov ah,2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出回车符
mov ah,0
mov al, [buffer+1] ; 将DI寄存器指向的数组元素的值加载到AX寄存器中
mov [Number], ax ; 将AX寄存器中的值存储到变量Number中
call print_uint16 ; 调用子程序print_uint16打印Number
; 程序结束
mov ah, 4Ch ; 设置DOS程序结束功能号,4C00h表示程序正常结束
int 21H ; 调用DOS中断21H,执行程序结束功能
end start ; 指定程序的结束点,并且程序从start标签开始执行
运行结果如下
冒泡排序
C语言版
升序排序,最小值左移
int arr[] = { 123, 45, 89, 192, 76, 158, 225, 34, 67, 189 };
int len = sizeof(arr) / sizeof(arr[0]);
int temp;
for (int i = 0; i < len; i++)
{
// 每次循环比较出较小的值,然后左移,循环完成后该轮的最小值就放到i指向的位置了
for (int j = len - 1; j > i; j--)
{
// 想降序排序就改为大于号
if (arr[j] < arr[j - 1])
{
temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
}
}
}
for (int i = 0; i < len; i++)
{
printf("%d, ", arr[i]);
}
汇编版
升序排序,最小值左移
.model small ; 定义程序的模型为small模型,代码段和数据段各占64KB
; 声明外部函数Lib_print_uint16,表示该函数在其他地方定义
extrn Lib_print_uint16:proc
.stack 100h ; 定义堆栈段的大小为256字节(100h = 256)
.data ; 数据段开始
arr db 123, 45, 89, 192, 76, 158, 225, 34, 67, 189 ; 定义一个包含10个无符号8位整数的数组
Number dw 0 ; 定义一个无符号16位整数变量Number,用于存储要打印的数字
.code ; 代码段开始
print_uint16: ; 定义一个子程序print_uint16
push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16
call Lib_print_uint16 ; 调用Lib_print_uint16函数,该函数负责打印指定长度的16位无符号整数
ret ; 返回
start: ; 定义程序的入口点
mov ax, @data ; 将数据段的起始地址加载到AX寄存器中
mov ds, ax ; 将AX寄存器中的地址复制到DS寄存器中,DS是数据段寄存器
; 冒泡排序算法的实现
mov cx,10 ; 将CX寄存器设置为10,表示数组的长度
mov di,0 ; 将DI寄存器设置为0,作为外层循环的计数器
for1:
push cx ; 保存CX寄存器的值,用于外层循环的计数
mov cx,9 ; 将CX寄存器设置为9,作为内层循环的计数,减少比较次数
for2:
cmp cx,di ; 比较CX和DI的值,判断是否需要继续内层循环
jna for2_end ; 如果CX小于或等于DI,跳转到for2_end结束内层循环
mov si,cx ; 将CX的值复制到SI,作为数组索引
mov al,arr[si] ; 将数组arr中索引为SI的元素加载到AL寄存器
cmp al,arr[si - 1] ; 比较AL和arr[SI-1]的值,判断是否需要交换
ja No_Swap ; 如果arr[si] 大于arr[si-1] ,跳转到No_Swap,不进行交换
;交换arr[si]和arr[si-1]
xchg arr[si - 1],al ; 将AL中的值与arr[si-1]交换,因为此时al中存放的是arr[si]的值
mov arr[si],al ; 此时al中的值是arr[si-1],放到arr[si]中,至此完成交换
No_Swap:
loop for2 ; 内层循环计数器CX减1,如果CX不为0,继续for2循环
for2_end:
pop cx ; 恢复CX寄存器的值,用于外层循环的计数
inc di ; 外层循环计数器DI加1
loop for1 ; 外层循环计数器CX减1,如果CX不为0,继续for1循环
; 打印排序后的数组
mov cx,10 ; 将CX寄存器设置为10,表示数组的长度
mov di,0 ; 将DI寄存器设置为0,作为数组索引
mov ax,0 ; 清空AX寄存器
s:
mov al,arr[di] ; 将数组arr中索引为DI的元素加载到AL寄存器
mov ah,0 ; 清空AH寄存器,确保AX是一个完整的16位无符号整数
mov [Number], ax ; 将AX寄存器中的值存储到变量Number中
call print_uint16 ; 调用子程序print_uint16打印Number的值
inc di ; 数组索引DI加1
loop s ; 内层循环计数器CX减1,如果CX不为0,继续s循环
; 程序结束
mov ah, 4Ch ; 设置DOS程序结束功能号,4C00h表示程序正常结束
int 21H ; 调用DOS中断21H,执行程序结束功能
end start ; 指定程序的结束点,并且程序从start标签开始执行
运行效果如下:
降序排序,最大值左移
将如下位置的代码由ja改为jb即可
运行效果如下:
C反汇编:函数调用
STOS:串行存储指令
作用:将ax中的数据,放到es:[di]指向的地址中,并且di自增(16位系统默认自增2,32位默认自增4)
格式:sots 数据类型 ptr es:[di]
其中数据类型就表示了di自增的数量,buty类型自增1,word类型自增2,... ...
示例
-
;将al中的值放到es:[di]指向的地址处,并且di自增1 stos byte ptr es:[di] stosb
上面两条指令等价。
下面的指令是上面指令的缩写
-
;将ax中的值放到es:[di]指向的地址处,并且di自增2 stos word ptr es:[di] stosw
-
;将eax中的值放到es:[di]指向的地址处,并且di自增4 stos dword ptr es:[di] stosd
这是在32位系统中的指令
REP:重复执行指令
作用: 会根据计数寄存器CX中的值来指定次数,重复执行指令
示例:将es:[0000h]到es:[0014h]之间的内存初始化为0xcccc
mov cx,10 ;指定rep要重复的次数
mov ax,0cccch ;指定要初始化的值
lea di,[0] ;指定要初始化的地址
rep stosw
运行后结果如下:
使用vs查看反汇编、内存、寄存器等
选择要查看的地方打上断点后运行,系统最好选择:x86,因为x86是32位,x64是64位,反汇编的代码会有所出入
然后选择:调试--->窗口,就可以选择打开反汇编、内存、寄存器等等界面了
最后效果如下
反汇编之函数调用
注意:之前在8086学的寄存器都是16位的,用ax、bx、cx这些表示。而在32位系统中这些寄存器都是32位的,用eax、ebx、ecx等等表示,寄存器前面加上e就说明该寄存器是32位的
以如下函数为例
// 定义
int My_Sum(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
// 调用
My_Sum(1, 2);
-
参数入栈,顺序是从左往右,然后跳转到函数所在的位置
push 2 push 1 call My_Sum
如下:
-
创建栈帧
push ebp ;bp寄存器的值入栈 mov ebp,esp ;sp赋值bp,让bp作为该栈帧的栈底 sub esp,0cch ;sp减去0xcc,表示该栈帧的大小为:0xcc ;地址寄存器入栈 push ebx push esi push edi lea edi,[ebp - 0ch] ;di指向距离栈底0x0c的地址,也就是说该函数可用的空间为12个字节(这里根据函数所需用到的空间来定) mov ecx,3 ;指定rep重复的次数 mov eax,0cccccccch ;指定初始化的值 rep stos dwrod ptr es:[edi] ;eax占4个字节,重复三次,刚好初始化从栈底开始的12个字节的空间为0xcc
如下:
-
创建局部变量
mov dword ptr [ebp - 8],0 ;从距离栈底8个字节位置开始创建四个字节的局部变量0x00000000
如下:
-
调用参数
mov eax,dword ptr [ebp + 8] ;取出参数:1([epb+4]是返回地址),并放到eax中 add eax,dword ptr [ebp + 12] ;取出参数:2,与eax相加后结果放在eax中 mov dword ptr [ebp - 8],eax ;将计算的结果放到刚刚创建的变量中
如下:
-
获取返回值
mov eax,dword ptr [ebp - 8] ;将返回结果放到eax中
-
函数返回
;恢复地址寄存器 pop edi pop esi pop edx add esp,0cch ;销毁栈帧(这也是为什么局部变量在函数运行完毕后会被销毁) mov esp,ebp pop ebp ;恢复ebp ret ;返回函数调用的地方 add esp,8 ;销毁传的参数,让esp和ebp回到调用函数前的状态
如下:
-
完整汇编如下:
遵循c反汇编的规则写打印函数
还是以前面打印uint16_t范围内的数字为例
; 声明该代码块可以被其他模块访问和调用
public Lib_print_uint16
.model small ; 使用小内存模型,代码段和数据段各64KB
.code
Lib_print_uint16 proc ; 定义一个名为Lib_print_uint16的过程
; 保存寄存器和设置栈帧
push bp ; 保存基址指针寄存器bp的值
mov bp, sp ; 将栈指针sp的值赋给bp,bp指向栈帧的栈底
sub sp, 16 ; 在栈上分配16字节的空间,用于局部变量
push bx ; 保存bx寄存器的值
push si ; 保存si寄存器的值
push di ; 保存di寄存器的值
; 设置附加段寄存器es
mov ax, ss ; 将栈段寄存器ss的值赋给ax
mov es, ax ; 将ax的值赋给附加段寄存器es,用于后续的内存操作
; 初始化局部变量空间
lea di, [bp - 16] ; 将局部变量的地址加载到di寄存器中
mov cx, 8 ; 设置循环次数为8,每次填充2个字节,8次就填充16个字节
mov ax, 0cccch ; 将0cccch加载到ax寄存器中
rep stos word ptr es:[di]; 将ax的值重复存入es:[di]指向的内存中,初始化局部变量空间
; 加载参数并准备转换
mov ax, word ptr [bp + 4]; 将要打印的数加载到ax寄存器中,准备进行除法操作
mov bx, word ptr [bp + 6]; 将要转换的进制数加载到bx寄存器中
mov di, bp ; 将bp的值赋给di,用于后续的字符存储
for1:
; 转换数字为字符
xor dx, dx ; 清空dx寄存器,准备进行除法操作
div bx ; 将dx:ax除以bx,商存储在ax中,余数存储在dx中
add dl, 30h ; 将余数转换为ASCII字符(0-9)
cmp dl, 58 ; 比较dl是否大于'9'的ASCII值
jb No_16 ; 如果dl小于'9',跳转到No_16
add dl, 7 ; 如果dl大于'9',加上7,转换为A-F的ASCII字符
No_16:
dec di ; 将di减1,指向下一个字符存储位置
mov byte ptr es:[di], dl ; 将转换后的字符存储到es:[di]指向的内存中
cmp ax, 0 ; 比较商ax是否为0
jne for1 ; 如果商ax不为0,继续循环
for2:
; 输出字符到屏幕
mov dl, byte ptr es:[di] ; 将es:[di]指向的字符加载到dl寄存器中
inc di ; 将di加1,指向下一个字符
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出dl中的字符
cmp di, bp ; 比较di是否为bp的初始值
jne for2 ; 如果di不为bp的初始值,继续循环,输出所有字符
; 输出换行符和回车符
mov dl, 0Ah ; 将0Ah(换行符)加载到dl寄存器中,准备输出换行符
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出换行符
mov dl, 0Dh ; 将0Dh(回车符)加载到dl寄存器中,准备输出回车符
mov ah, 2 ; 设置ah为2,这是DOS中断21h中用于输出字符的服务号
int 21h ; 调用DOS中断21h,输出回车符
; 恢复寄存器和栈帧
pop di ; 恢复di寄存器的值
pop si ; 恢复si寄存器的值
pop dx ; 恢复dx寄存器的值
add sp, 16 ; 释放栈上分配的16字节空间
mov sp, bp ; 将bp的值赋给sp,恢复栈指针
pop bp ; 恢复基址指针寄存器bp的值
ret ; 返回调用处
Lib_print_uint16 endp ; 结束过程定义
end ; 标记程序结束
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库