OS专题-了解函数和字符串

OS专题-了解函数和字符串

为了更高效的开发,我们肯定需要将一些公共代码封装起来,这样就可以减少很多代码量。为此我们需要了解函数这个概念。

字符串

首先先来说一下字符串吧!他也是非常常用的一个东西。我们知道在C语言中,字符串就是字符数组组成的,并且最后一位总是\0。汇编中也是一样的,一般情况下我们定义字符串是这样定义的:

mystring:
    db 'Hello, World', 0

上面的代码被单引号包围的文本会被汇编器转换成ASCII码,最后那个0会被直接当成0x00处理。

控制流程

我们还要学习一些控制流程,不然我们写不出向C那样的条件分支语句和循环语句。

之前我们已经学习过一个控制语句了,就是jmp $!它的作用是跳转到当前语句位置继续执行,说白了就是死循环。跳转语句非常有用,就是它实现了我们C中的分支语句。汇编中的跳转条件是基于 上一条 指令的计算结果来执行的。例如:

cmp ax, 4      ; 这里是比较两个数
je ax_is_four  ; 根据比较结果来决定是否跳转
jmp else       ; 若上面的条件不成立就跳转到else
jmp endif      ; 结束一个if分支

ax_is_four:
    .....
    jmp endif

else:
    .....
    jmp endif  ; 这一行其实可有可无,为了好看统一写上去的

endif:

这些东西都非常重要,了解了这些才能完成较为复杂的逻辑语句。里面的汇编指令需要各位自行百度或Google,有时间我会整理一份指令说明的~

函数

现在我们来聊聊函数,和你预想的一样,他跟我们调用字符串没什么区别,调用函数就是跳转到函数对应的位置并执行。这里有一个值得注意的点,就是如何传递参数?我们可以用两种传递方式:

  1. 事先规定使用某一个寄存器或地址来共享数据
  2. 需要更多的代码逻辑,抽象一个共享区域

第一种很简单,我们规定使用AX寄存器来存储参数

mov al, 'X'
jmp print
endprint:

...

print:
    mov ah, 0x0e  ; tty code
    int 0x10      ; I assume that 'al' already has the character
    jmp endprint  ; this label is also pre-agreed

上述代码虽然实现了参数传递,但是这个代码却不能复用。因为print函数中存在跳转回去的代码,所以只能在这里使用。那该怎么修改呢?我们需要改进两处位置:

  • 通过一个寄存器或空间保存需要返回执行的代码地址
  • 保存寄存器的当前状态,让子函数可以修改寄存器,而不影响其他的函数调用

这些功能CPU都已经实现,我们可以使用call指令和ret指令来代替jmp指令完成对函数的调用。用pushapopa指令来保存和恢复寄存器信息。

这里提一个小知识点,我们可以像C语言那样将不同的模块分成不同的文件。
在需要的地方用%include "file.asm"去引入这个文件

实现打印函数

现在我们来封装一下打印函数,新建两个文件lib/print.asmlib/print_hex.asm。代码如下:

; lib/print.asm
print:
    pusha ;保存之前的状态

; 记住这个C代码:
; while (string[i] != 0) { print string[i]; i++ }

; 实现循环判断
start:
    mov al, [bx] ; 将BX寄存器指向的数据取出存入AX
    cmp al, 0 ; 比较是否为字符串的结尾标记 \0
    je done ; 根据比较结果执行,如果是就跳转到结束位置停止执行

    ; 不是我们开始打印
    mov ah, 0x0e
    int 0x10 ; 中断触发打印

    ; 让指针加1,指向下一个字符
    add bx, 1
    jmp start ; 返回到开头继续执行

done:
    popa
    ret


print_nl:
    pusha
    
    mov ah, 0x0e
    mov al, 0x0a ; ASCII换行符
    int 0x10
    mov al, 0x0d ; ASCII回车指令
    int 0x10
    
    popa
    ret


; lib/print.asm
; 接收DX寄存器中的数据
; 假设dx=0x1234
print_hex:
    pusha

    mov cx, 0 ; 指针变量

; 获取DX中的字节数据
; 因为数字0在ASCII中的数据值是`0x30`,9是`0x39`,所以让`0x30`作为数字基准
; 字符A-F对应的值为`0x41`-`0x46`,所以让`0x40`作为大于10之后的数字基准
; 通过这样的基准运算,我们就可以计算出数值对应的字符值。
hex_loop:
    cmp cx, 4 ; 我们需要循环四次,满足就结束循环
    je end
    
    mov ax, dx ; 我们让AX寄存器作为工作寄存器
    and ax, 0x000f ; 屏蔽其他值只保留最后4位的数据
    add al, 0x30 ; 将其转换为数字字符的ASCII
    cmp al, 0x39 ; 比较一下看看是不是大于数字9了
    jle step2 ; 不大于就跳过下面的处理指令
    add al, 7 ; 需要额外进行ASCII转换,才能表示出字母A-F

step2:
    mov bx, HEX_OUT + 5 ; 取到HEX_OUT模板的对应地址
    sub bx, cx  ; 减去下标获取地址
    mov [bx], al ; 给对应的位置赋值
    ror dx, 4 ; 循环右移4位 举例:0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234

    ; 给循环标志加1
    add cx, 1
    jmp hex_loop

end:
    ; 循环结束后我们输出HEX_OUT模板就是我们想要的结果了
    mov bx, HEX_OUT
    call print

    popa
    ret

HEX_OUT:
    db '0x0000',0 ; 在内存中开辟空间存放字符串

修改mbr.asm中的代码,调用上述函数:

[org 0x7c00] ; 告诉编译器我们的基准地址

; 函数调用打印数据
mov bx, HELLO
call print

call print_nl

mov bx, GOODBYE
call print

call print_nl

mov dx, 0x12fe
call print_hex

; 无限循环
jmp $

; 导入函数文件
%include "print.asm"
%include "print_hex.asm"


; 数据
HELLO:
    db 'Hello, World', 0

GOODBYE:
    db 'Goodbye', 0

; 填充其他位的值为00
times 510-($-$$) db 0
; 魔数
dw 0xaa55

若你真的这么做了…那么代码编译时会出现如下错误:

error: unable to open include file `print.asm': No such file or directory
error: unable to open include file `print_hex.asm': No such file or directory

这是因为我们没有告诉编译器,这些函数文件的位置,我们需要修改一下CMakeLists.txt文件。在add_executable(loader boot/mbr.asm)前面增加include_directories(lib)即可。


OS专题-了解函数和字符串
https://blog.cikaros.top/doc/93fdb9ff.html
作者
Cikaros
发布于
2022年9月23日
许可协议