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,有时间我会整理一份指令说明的~
函数
现在我们来聊聊函数,和你预想的一样,他跟我们调用字符串没什么区别,调用函数就是跳转到函数对应的位置并执行。这里有一个值得注意的点,就是如何传递参数?我们可以用两种传递方式:
- 事先规定使用某一个寄存器或地址来共享数据
- 需要更多的代码逻辑,抽象一个共享区域
第一种很简单,我们规定使用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
指令完成对函数的调用。用pusha
和popa
指令来保存和恢复寄存器信息。
这里提一个小知识点,我们可以像C语言那样将不同的模块分成不同的文件。
在需要的地方用%include "file.asm"
去引入这个文件
实现打印函数
现在我们来封装一下打印函数,新建两个文件lib/print.asm
和lib/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)
即可。