打印:hello world
在屏幕上输出字符
mov dl,'a' ; 将要打印的字符放到dl中
mov ah, 02h ; 设置显示字符的功能号
int 21h ; 调用DOS中断,打印字符
在屏幕上输出字符串
mov ah,09h ;设置显示字符串的功能号
int 21H ;调用BIOS中断
字符串的结束符:$
在一个段中定义要打印的字符串,并在字符串结尾写上'$',当输出字符的中断遇到"$"时,就会停止打印字符
data segmentstr db 'Hello World', 0dh, 0ah, '$' ;在data段定义要打印的字符串;其中0dh和0ah为回车和换行符;'$'为字符串结束符
data ends
打印字符串
data segmentstr db 'Hello World', 0dh, 0ah, '$' ;在data段定义要打印的字符串;其中0dh和0ah为回车和换行符;'$'为字符串结束符
data endscode segment;关联code段和data段的段地址寄存器assume cs:code, ds:datastart:;关联data段mov ax,datamov ds,axmov dx,offset str ;设置dx指向str字符串的起始位置mov ah,09h ;设置显示字符串的功能号int 21H ;调用BIOS中断;退出程序mov ah,4CHint 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; //设置结果初值为1while (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 endsstack segment;定义栈空间dw 10 dup(0)
stack endscode segment;关联code段和data段的段地址寄存器assume cs:code, ds:data, ss:stack;定义可以在屏幕上打印uint16范围内的数字的函数print_uint16:;先清空str中的数据mov si,offset strmov cx,5mov ax,32Clear: mov [si],ax ;32对应的ascll是空格inc siloop Clearmov cx,[Len] ;要打印的数字长度s: push cx ;保存循环计数器dec cxmov [Result], 1 ;初始化除数;次方函数Serial_Pow: cmp cx,0je s2mov ax,10 ;计算16位的乘法,其中一个乘数要在ax中mul [Result] ;计算Result乘以10mov [Result],ax ;16位的乘法低16位在ax中,将其移到Result中,因为Result不会超过16位,所以不用管高16位loop Serial_Pows2: pop cx ;恢复循环计数器;计算16位的除法,被除数必须放在在dx和ax中,除数在Result中mov dx,0mov ax,[Number] ;将32位被除数放入ax和bx中div [Result] ;对Number取整,取整的结果在ax中,余数在dx中;ax中存放的是取整后的结果,将dx清零后可再次进行除法运算mov dx,0 ;清除余数,准备对取整后的结果再进行取余mov bx,10div bx ;对十进制数取余,余数在dx中add dl,30h ;将余数转换为ASCII码;将数字高位放到左边,低位放到右边mov ax,[Len]mov [var],axsub [var],cxmov bx,[var]mov [str+bx],dlloop s;打印字符串mov dx,offset str ;设置显示字符串的偏移地址mov ah,09h ;设置显示字符串的功能号int 21H ;调用BIOS中断ret;程序开始,相当于main函数start: ;关联段与段寄存器mov ax,datamov ds,axmov ax,stackmov ss,ax;相当于给print_uint16函数传参mov [Number],12345 ;在屏幕上打印12345mov [Len],5;调用print_uint16函数call print_uint16mov [Number],65535 ;在屏幕上打印65535mov [Len],5call print_uint16mov [Number],777 ;在屏幕上打印777mov [Len],3call print_uint16mov [Number],1 ;在屏幕上打印00001mov [Len],5call print_uint16;退出程序mov ah,4CHint 21H
code ends
end start
运行后效果如下:
优化版
该版本不用设置数字长度,只需要将要打印的uint16_t类型的数字放到Number中即可
先看分文件编写后再看这个
; 声明该代码块可以被其他模块访问和调用
public Lib_print_uint16.model small ; 使用小内存模型,代码段和数据段各64KB
.stack 100h ; 定义堆栈段大小为256字节 (100h = 256).dataNumber dw 0 ; 定义一个字(16位)变量Number,初始值为0Result dw 0 ; 定义一个字(16位)变量Result,用于存储数字取整所要除的数var dw 0 ; 定义一个字(16位)变量var,用于在程序运行时临时存储数据.codeLib_print_uint16 proc; 从栈中获取Number的地址pop [var] ; 获取返回地址到var中,因为是近距离转移,所以只把偏移地址压入栈中,pop一次就够了pop [Number] ; 将要打印的数字放到Number中push [var] ; 将返回地址重新压入栈中; 保存寄存器状态push axpush bxpush cxpush dxpush sipush dimov bx, 10 ; 计算十进制的位数,用10对待计算的数取整mov cx, 0 ; 初始化cx寄存器为0,cx将用于计数Number的位数mov ax, [Number] ; 将Number的值加载到ax寄存器中cmp ax, 0 ; 比较ax寄存器中的值是否为0jne 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寄存器的值,计数器加1mov dx, 0 ; 清空dx寄存器,用于除法操作div bx ; 将ax寄存器中的值除以bx寄存器中的值,ax = ax / bx, dx = ax % bxcmp ax, 0 ; 比较ax寄存器中的值是否为0jne Count_Bits ; 如果ax不为0,继续循环; 开始循环打印数字,数字长度已经在cx中
s:push cx ; 保存循环计数器dec cx ; 将cx减1,用于计算当前位数mov [Result], 1 ; 初始化除数Result为1
Serial_Pow:cmp cx, 0 ; 比较cx和0je s2 ; 如果cx等于0,跳转到s2mov ax, 10 ; 将10加载到ax寄存器mul [Result] ; 计算Result乘以10,结果低16位在ax,高16位在dxmov [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 ; 标记程序结束
再次优化版
该版本可以指定进制
对应的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).datastr db 16 dup(0) ; 存储待打印的字符,初始化为16个0字节的空间scale dw 0 ; 进制,这里用于设置要转换的数字的进制,默认为0Number 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是否为0jne 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是否为0jne 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 ; 标记程序结束
分文件编写
内存模型
在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,@datamov ds,ax;退出程序mov ah,4chint 21hend start
等价于:
;关联段与段寄存器 assume cs:code, ds:data, ss:stack stack segment ;定义栈段dw 256 dup(0) ;定义栈段的大小为256个字节,用0填充 stack endsdata segment ;定义数据段d1 db 10 dup(0) data endscode segment ;定义代码段start:;将ds段寄存器指向data段mov ax,datamov ds,ax;将ss段寄存器指向stack段mov ax,stackmov ss,ax;退出程序mov ah,4chint 21h data endsend 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).datastr db 16 dup(0) ; 存储待打印的字符,初始化为16个0字节的空间scale dw 0 ; 进制,这里用于设置要转换的数字的进制,默认为0Number 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是否为0jne 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是否为0jne 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_uint16push [scale]push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16call 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打印Numbermov [scale],10 ; 转成10进制后打印call print_uint16 ; 调用子程序print_uint16打印Numbermov [scale],8 ; 转成8进制后打印call print_uint16 ; 调用子程序print_uint16打印Numbermov [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 segmentstr db 'Hello World', 0dh, 0ah, '$' ;在data段定义要打印的字符串;其中0dh和0ah为回车和换行符;'$'为字符串结束符
data endscode segment;关联code段和data段的段地址寄存器assume cs:code, ds:data;定义print子程序,打印str字符串print:mov dx,offset str ;设置dx指向str字符串的起始位置mov ah,09h ;设置显示字符串的功能号int 21H ;调用BIOS中断retstart:;关联data段mov ax,datamov ds,ax;调用print子程序打印字符串,原始字符串call print;将str字符串中的所有字母转换为大写字母mov si,offset str ;设置si指向str字符串的起始位置mov cx,11 ;设置cx为字符串长度s1: mov al,[si] ;将si指向的字符赋值给aland al,0dfh ;将al中的字符中的第5位(大写字母的第5位为0)置为0mov [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指向的字符赋值给alor al,20h ;将al中的字符中的第5位(小写字母的第5位为1)置为1mov [si],al ;将al中的字符赋值给si指向的位置inc si ;指向下一个字符的地址loop s2 ;循环执行s2,直到将替换完所有字符;调用print子程序打印字符串,转换为小写的字符串call print;退出程序mov ah,4CHint 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 endscode segment;关联code段和data段的段地址寄存器assume cs:code, ds:data;定义print子程序,打印str字符串print:mov dx,offset str ;设置显示字符串的偏移地址mov ah,09h ;设置显示字符串的功能号int 21H ;调用BIOS中断retstart:;关联data段mov ax,datamov ds,axmov 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 sumadd ah,30h ;将最大值转换为ASCII码mov [str+15],ah ;替换字符串的占位符为最大值call print ;调用print子程序打印最大值;退出程序mov ah,4CHint 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, 0Number dw 0 ; 用于存储要打印的数字.code ; 代码段开始print_uint16: ; 定义一个子程序print_uint16push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16call 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 ; 设置Numbercall 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_uint16push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16call 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打印Numberloop 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_uint16push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16call Lib_print_uint16 ; 调用Lib_print_uint16函数,该函数负责打印指定长度的16位无符号整数ret ; 返回start: ; 定义程序的入口点mov ax, @data ; 将数据段的起始地址加载到AX寄存器中mov ds, ax ; 将AX寄存器中的地址复制到DS寄存器中,DS是数据段寄存器lea dx,buffermov ah,0ahint 21hmov 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,0mov 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_uint16push [Number] ; 将Number的值压入堆栈,作为参数传递给Lib_print_uint16call 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加1loop 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加1loop 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 edilea 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 edxadd esp,0cch ;销毁栈帧(这也是为什么局部变量在函数运行完毕后会被销毁) mov esp,ebp pop ebp ;恢复ebp ret ;返回函数调用的地方 add esp,8 ;销毁传的参数,让esp和ebp回到调用函数前的状态
如下: