从0创建一个OS (六) 汇编函数和控制结构

本文详细介绍了如何在汇编语言中创建控制结构、函数,包括字符串处理、条件跳转和函数调用。通过实例展示了如何编写print函数以及处理16进制数的打印。同时强调了在汇编中包含外部文件的注意事项,尤其是%include语句的位置对其执行顺序的影响。
摘要由CSDN通过智能技术生成

本节将学习汇编函数和控制结构,其中穿插着字符串打印、数字转字符知识.

关键字: 控制结构; 函数调用; 字符串

目标: 学习使用汇编语言编写控制结构、函数

对于本节的控制结构和函数,读者应该在本系列博客的第一篇原文介绍+环境搭建中,提到的《汇编语言(第三版)》一书中学习到过,因此本节的目地也是复习相关知识,为我们能写出一个功能完备的boot sector做准备.

理论基础

字符串

在汇编中定义一个类似于C语言中的字符串,即一系列的字符和结尾的一个0,可以用如下方式定义

; db表示后面的字符串中每一个字符占一个字节(byte),类似的数据类型还有dw,dd等
db 'Hello, World', 0

注意单引号扩起来的字符串会被编译器逐一转换为ASCII码存储起来,而最后的0被存储为ASCII码的0.

控制结构

所谓控制结构,就是控制代码运行顺序的语句,C语言中的for、while、switch、break…都为控制结构语句.

汇编语言方面,本系列博客之前用到过jmp语句,后面跟上$表示死循环,当然jmp还有很多衍生语句,根据上一条语句的执行结果,进行不同的跳转处理,它们被称为条件跳转命令.

这里给出一个条件跳转的例子


cmp ax, 4 ; 对比ax是否等于4
je ax_is_four ; 如果相等,则跳转到一个代码位置
jmp else ; 如果不相等,则跳转到另一个代码位置
jmp endif ; 最后,进入正常执行流

ax_is_four:
	...
	jmp endif

else:
	...
	jmp endif

endif:
	...
	

本节只使用一种条件跳转命令,即jle,表示如果上一条语句的结果为"<=",则跳转.如果需要其它跳转指令,可以到x86条件跳转指令表查询.

函数调用

汇编语言中的函数调用,其实只是跳转到一个标签(label),所谓标签,就是类似于上面例子中的“ax_if_four、else、endif”,用来标记代码位置.

比较特殊的是传参,汇编传参一般有2步:

  1. 将参数放入特定的寄存器,并编写文档告知调用者,应该将参数放入哪个(些)寄存器
  2. 稍稍多写一点代码,保证编写的函数能够允许调用者随意调用

进入函数后应该先保存寄存器环境,防止在函数中改写寄存器,返回后导致主环境中寄存器内容混乱,函数返回前将保存的寄存器恢复.

以上要求我们使用CPU提供的指令来实现.

将系统寄存器保存在栈中: pusha
将系统寄存器恢复到进入函数之前的状态: popa
返回到主函数: ret

现在我们来编写一个print函数,来实现打印单个字符的功能.

; =============================================
; 函数名: print
; 函数功能: 打印传入的字符
; 参数: AL,调用者应将需要打印的字符传入8bit寄存器AL中
; =============================================

print:
	pusha
	mov ah, 0x0E
	int 0x10
	popa
	ret

包含外部文件

如果我们编写了一类函数,专门用来打印各种类型的数据,如字符串,字符…,这里假设我们写的这类函数的文件叫做print_class.asm.

通用的做法是在需要调用打印函数的源文件中包含print_class.asm.

在C语言中我们的做法为

#include "print_class.asm"

在汇编中我们的做法为

%include "print_class.asm"

注意!汇编中的%include语句的位置放置比较讲究,不同于C语言中只需要将#include放置在文件开头就万事大吉,汇编需要将%include放置在数据段/主程序的代码段之后,目地是不影响主程序的正常执行. 详细情况我在本节的最后会向大家介绍.

打印16进制数

如果直接将16进制数进行打印,那么就会出现打印错误的情况,因为打印函数默认获得的是ASCII码,而ASCII码和数字并不一一对应,因此我们需要提前将16进制数转化为对应的ASCII码.

将16进制数转化为对应的字符形式是有规律可循的,这里直接给出规律,读者可以自行参照ASCII码表进行验证.

情况一: 0~9的16进制数 + 0x30 = 对应ASCII码
情况二: A~F的16进制数 + 0x37 = 对应ASCII码

源码

boot_sect_print.asm
包含print和println两个函数,用以实现打印字符串和打印换行归位

; ===========================================================
; 函数名: print
; input: bx
; input的意义: 需要打印的字符串的首地址
; 效果:打印以bx为首地址的字符串
; ===========================================================
print:
    pusha

;while(string[i] != 0) { print string[i]; i++} 为指导思想


start:
    ; 确认是否为字符串结尾(值为0的字节)
    mov al, [bx]
    cmp al, 0
    je done

    ; 没到结尾则打印字符, 并跳转到字符串的下一个字符
    mov ah, 0x0E
    int 0x10
    inc bx ; bx = bx + 1, 也可以使用 add bx, 1
    jmp start

done:
    popa
    ret

; ===========================================================
; 函数名: print_nl
; input: 没有输入
; 效果:换行并将光标置到第一列
; ===========================================================

print_nl:
    pusha

    mov ah, 0x0E
    mov al,  0x0A ; 0x0A是换行键的ASCII码,但只会将光标置到下一行的当前列,而不是第一列
    int 0x10
    
    mov al, 0x0D ; 0x0D是归位键的ASCII码,作用为将光标回到第一列
    int 0x10

    popa
    ret

boot_sect_print_hex.asm
包含print_hex函数,用于打印16进制数

; ===========================================================
; 函数名: print_hex
; input: dx
; input的意义: 需要打印的16bit的16进制数
; 效果:打印存储在dx中的16进制数
; ===========================================================

print_hex:
    pusha

    mov cx, 0
    
    ; 对于0~9的数字,转换为字符0~9,只需在数字上加0x30,再打印字符即可
    ; 对于A~F(10~15)的数字,转换为字符A~F,只需在数字上加0x37即可

hex_loop:
    cmp cx, 4 ; 循环4次
    je end

    ; 16进制数的每一位用4bit二进制表示,如0x1234可以用二进制表示为 0001 0010 0011 0100
    ; 因此我们的思路为,利用16bit寄存器AX,获取DX中每4个bit的二进制数,将其转换为对应字符的ASCII码
    ; 并按顺序放置在我们提前准备好的一个32bit的空间(HEX_OUT)(严格讲db "0x0000", 0 共占用56bit)
    mov ax, dx
    and ax, 0x000F
    add al, 0x30
    cmp al, 0x39 ; 0x39是字符'9'的ASCII码
    jle step2 ; 如果ASCII码<='9'的ASCII码
    add al, 7 ; 如果是A~F,应该在加0x30的基础上再多加7

    ;-----16进制的一位(09,A~F),转换为对应字符的ASCII码成功-----
    

step2: 
    mov bx, HEX_OUT + 5 ; 此时bx指向HEX_OUT的0x0000的最低位的0

    ; 利用cx定位0x0000中的右侧40, 循环中cx取值依次为0,1,2,3,对应bx指向0x0000右侧的第43210
    sub bx, cx

    mov [bx], al

    ; 也可以使用ror dx, 4  意为: 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234
    shr dx, 4 ; 0x1234 -> 0x0123 -> 0x0012 -> 0x0001 -> 0x0000
    
    add cx, 1
    jmp hex_loop

end:
    mov bx, HEX_OUT
    call print

    popa
    ret

HEX_OUT:
    db '0x0000', 0

boot_sect_main.asm
主函数

[org 0x7C00] ; 变量寻址基址从0x7C00(boot sector被load的地址)开始



; 向寄存器传参数,并调用函数
mov bx, HELLO
call print

call print_nl

mov bx, GOODBYE
call print

call print_nl

mov dx, 0x12fe
call print_hex

call print_nl


; 无限循环
jmp $

%include "boot_sect_print.asm"
%include "boot_sect_print_hex.asm"

HELLO:
    db 'Hello, World', 0

GOODBYE:
    db 'Goodbye', 0

times 510 - ($ - $$) db 0
dw 0xAA55

编译并Boot

本系列博客自本节之后不再对编译并Boot作说明,如果没有特别提到,默认使用nasm编译,qemu启动仿真.

实验结果

在这里插入图片描述

番外

英文原文教程在主函数中对于%include的注释为:
在这里插入图片描述
它说要把%include语句放在无限循环之后,嘿,译者尝试将%include放在org 0x7c00后面,结果啥都没有输出,我就很奇怪了(其实是我蠢),于是乎上网搜索,发现在stackoverflow里有位兄弟跟我一样stupid.

贴上链接nasm-instruction-sequence,回答者很明显游刃有余,说明了原因.

针对该汇编程序来说,如果将%include放在无限循环之前,那么将会导致在执行主程序时先执行你的boot_sect_print.asm和boot_sect_print_hex.asm中的内容,可是都还没往函数里传参,AL中都是乱码,自然打印出来的东西乱七八糟了,所以你这个程序的%include只能往无限循环后面放.

一般的,必须将%include语句放在数据段/非主程序段,总而言之就是不能让它主动执行,要让它被动调用

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值