六、汇编实战

打印: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

运行后效果如下:

image

打印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

运行后效果如下:

image

优化版

该版本不用设置数字长度,只需要将要打印的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)的大小和访问方式。

不同的内存模型适用于不同大小的程序和不同的内存管理需求。

  1. 紧凑型内存模型(.model compact)

    • 段的大小

      • 代码段(CS): 64KB
      • 数据段(DS): 64KB
      • 堆栈段(SS): 64KB
      • 附加段(ES): 64KB
    • 特点

      • 代码段和数据段可以共享同一个64KB的地址空间。
      • 堆栈段和附加段可以共享同一个64KB的地址空间。
      • 使用近指针(near pointers)和远指针(far pointers)。
      • 适用于需要在数据段和堆栈段之间共享内存的程序。
  2. 中型内存模型(.model medium)

    • 段的大小

      • 代码段(CS): 1MB
      • 数据段(DS): 64KB
      • 堆栈段(SS): 64KB
      • 附加段(ES): 64KB
    • 特点

      • 代码段可以超过64KB,达到1MB。
      • 数据段、堆栈段和附加段每个都限制在64KB的范围内。
      • 使用远指针(far pointers)。
      • 适用于代码量较大但数据量较小的程序。
  3. 大型内存模型(.model large)

    • 段的大小

      • 代码段(CS): 1MB
      • 数据段(DS): 1MB
      • 堆栈段(SS): 1MB
      • 附加段(ES): 1MB
    • 特点

      • 所有段都可以达到1MB的大小。
      • 使用远指针(far pointers)。
      • 适用于大型程序,需要更大的代码和数据段。
      • 段寄存器(CS、DS、SS、ES)可以独立指向不同的段。
  4. 巨大内存模型(.model huge)

    • 段的大小

      • 代码段(CS): 1MB
      • 数据段(DS): 1MB
      • 堆栈段(SS): 1MB
      • 附加段(ES): 1MB
    • 特点

      • 类似于大型内存模型,但主要用于处理非常大的数据结构。
      • 使用远指针(far pointers)。
      • 适用于需要处理非常大的数据结构的程序。
  5. 大型紧凑型内存模型(.model large compact)

    • 段的大小

      • 代码段(CS): 1MB
      • 数据段(DS): 64KB
      • 堆栈段(SS): 64KB
      • 附加段(ES): 64KB
    • 特点

      • 代码段可以达到1MB。
      • 数据段、堆栈段和附加段每个都限制在64KB的范围内。
      • 使用远指针(far pointers)。
      • 适用于代码量较大但数据量较小的程序,数据段和堆栈段可以共享64KB的地址空间。
  6. 大型紧凑型内存模型(.model large compact)

    • 段的大小

      • 代码段(CS): 1MB
      • 数据段(DS): 64KB
      • 堆栈段(SS): 64KB
      • 附加段(ES): 64KB
    • 特点

      • 代码段可以达到1MB。
      • 数据段、堆栈段和附加段每个都限制在64KB的范围内。
      • 使用远指针(far pointers)。
      • 适用于代码量较大但数据量较小的程序,数据段和堆栈段可以共享64KB的地址空间。
  7. 中型紧凑型内存模型(.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 伪指令来定义,并且必须为其分配一个名字(有效标识符) 。感觉也可以叫他函数

  • 语法:

    1. 近距离调用(不跨段)

      过程名 proc
      	...		;具体的功能代码
      	ret
      过程名 endp
      
    2. 远距离调用(跨段)

      过程名 proc far
      	...		;具体的功能代码
      	retf	;注意返回符的不同
      过程名 endp
      
  • 注意:

    1. 定义在过程中的标号只有过程内可见。

      标号类似于变量,过程类似函数,函数中定义的变量为局部变量,只有函数内可访问。标号之于过程与变量之于函数是一样的道理

    2. 如果想让过程中定义的标号全局可见,只需要多写一个冒号,如下

      过程名 proc
      	
      	标号1:		;该标号只有过程内部可见
      	
      	标号2::		;该标号全局可见
      	
      	ret
      过程名 endp
      

PUBLIC:声明一个标号可被其他模块调用

作用: 汇编伪指令,用于说明程序模块中的某个标号是可以被其他程序模块调用的。

格式:public 标号

EXTRN:声明一个标号是使用其他模块中的

作用: 汇编伪指令,用于说明程序模块中用到的标号是其他程序模块的

格式:

  1. extrn 标号:类型

    其中类型包括: near、far、byte、word、dword等。

    • near: 近调用,过程地址在同一个代码段内。

    • far: 远调用,过程地址可以位于不同的代码段内。

    • byte: 声明一个字节(8位)变量。

    • word: 声明一个字(16位)变量。

    • dword: 声明一个双字(32位)变量。

    • ...

  2. extrn 标号

    • smallcompact 内存模型中,默认类型为 near
    • largehuge 内存模型中,默认类型为 far
    • 对于数据变量(如 byte, word, dword),如果不指定类型,默认为 word

示例:将打印uin16_t范围的数字独立到另一个文件中

将上面打印uint16_t范围内的数字的代码放到单独的一个文件中,实现模块化编程

在同级目录中创建main.asmprint.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
    
  • 运行结果如下:

    image

转换字母的大小写

通过观察ASLLC码可知,字母大写与小写只有第五位不一样,如下

image

当第五位为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

运行后结果如下

image

求数组的最大和最小值

这里的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

运行结果如下:

image

求最小值同理,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标签开始执行

运行结果如下:

image

计算斐波那契数列

.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标签开始执行

运行结果:

image

获取输入的字符串数量

获取键盘输入的字符串

  1. 在数据段定义输入缓冲区,用来存放输入的数据

    .data                               ; 数据段开始
        buffer db 255, 0, 255 dup(0)    ;定义输入缓冲区
    

    缓冲区第一个字节表示最大可接收的字符数量

    第二个字节表示实际接收到的字符数量

    第三个字节开始存储接收到的字符

  2. 将输入缓冲区的地址加载到dx中

    lea  dx,buffer
    ;等价于:mov dx,offset buffer
    
  3. 调用输入中断

    mov    ah,0ah                   ; 设置ah为0ah,这是DOS中断21h中用于输入字符串的服务号
    int    21h                      ; 调用DOS中断21h,执行字符串输入操作
    
  4. 在输入完成后记得打印回车换行,避免程序后面要打印字符时与输入的字符重叠

    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标签开始执行

运行结果如下

image

冒泡排序

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标签开始执行

运行效果如下:

image

降序排序,最大值左移

将如下位置的代码由ja改为jb即可

image

运行效果如下:

image

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

运行后结果如下:

image

使用vs查看反汇编、内存、寄存器等

选择要查看的地方打上断点后运行,系统最好选择:x86,因为x86是32位,x64是64位,反汇编的代码会有所出入

image

然后选择:调试--->窗口,就可以选择打开反汇编、内存、寄存器等等界面了

image

最后效果如下

image

反汇编之函数调用

注意:之前在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);
  1. 参数入栈,顺序是从左往右,然后跳转到函数所在的位置

    push 2
    push 1
    call My_Sum
    

    如下:

    image

  2. 创建栈帧

    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
    

    如下:

    image

  3. 创建局部变量

    mov dword ptr [ebp - 8],0	;从距离栈底8个字节位置开始创建四个字节的局部变量0x00000000
    

    如下:

    image

    image

  4. 调用参数

    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		;将计算的结果放到刚刚创建的变量中
    

    如下:

    image

    image

  5. 获取返回值

    mov eax,dword ptr [ebp - 8]		;将返回结果放到eax中
    

    image

  6. 函数返回

    ;恢复地址寄存器
    pop edi
    pop esi
    pop edx
    
    add esp,0cch	;销毁栈帧(这也是为什么局部变量在函数运行完毕后会被销毁)
    mov esp,ebp
    pop ebp			;恢复ebp
    ret				;返回函数调用的地方
    add esp,8		;销毁传的参数,让esp和ebp回到调用函数前的状态
    

    如下:

    image

    image

  7. 完整汇编如下:

    image

    image

遵循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                          ; 标记程序结束
posted @   7七柒  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示