手写操作系统(十六)-实现整数打印

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

1.完整代码

在print.S一开始定义

section .data 
put_int_buffer dq 0             ;定义8字节缓冲区用于数字到字符的转换

在前面代码的基础上添加逻辑

;------------将小端字节序的数字变成对应的ASCII码后,倒置--------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x
;------------------------------------------------------------------
global put_int
put_int:
    pushad
    mov ebp,esp
    mov eax,[ebp+4*9]           ;将参数写入eax中,call返回地址占4B+pushad的8个4B
    mov edx,eax                 ;eax存储的是参数的备份,edx为每次参与位变换的参数,当转换为16进制数字后,eax将下一个参数给edx
    mov edi,7                   ;指定在put_int_buffer中初始的偏移量,表示指向缓冲区的最后一个字节
    mov ecx,8                   ;32位数字中,每4位表示一个16进制数字。所以32位可以表示8个16进制数字,位数为8。
    mov ebx,put_int_buffer      ;ebx为缓冲区的基址
;将32位数字按照16进制的形式从低到高逐个处理,共处理8个16进制数字
.16based_4bits:
    ;将32位数字按照16进制形式从低到高逐字处理
    and edx,0x0000_000F          ;解析16进制数字的每一位,and后edx只有低4位有效(最低位的16进制数字)
    cmp edx,9                   ;数字0~9和a~f需要分别处理成对应的字符
    jg .is_A2F                  ;jg:Jump if Greater,若大于9,则跳转.is_A2F
    add edx,'0'                 ;如果是0~9,则加上'0'的ASCII码
    jmp .store
.is_A2F:
    sub edx,10                  ;A~F减去10所得的差,10的ASCII码为1
    add edx,'A'                 ;加上10的ASCII码得到字符的ASCII码
;将每个数字转换成对应的字符后,按照类似大端的顺序存储到缓冲区put_int_buffer中。
;高位字符放在低地址,低位字符放在高地址,这样和大端字符序类似。
.store:
    ;此时dl中应该是对应数字的ASCII码
    mov [ebx+edi],dl
    dec edi
    shr eax,4                   ;右移4位,去掉最低4位
    mov edx,eax
    loop .16based_4bits
;现在把put_int_buffer中已全是字符,打印之前把高位连续的字符去掉。
;例如:000123 -> 123
.ready_to_print:
    inc edi                     ;此时edi为-1(0xffff_ffff),加1使其为0
.skip_prefix_0:
    cmp edi,8                   ;若以及比较到第9个字符,表示待打印的字符都是0
    je .full0                   ;Jump if Equal
;找出连续的0字符,edi作为非0的最高位字符的偏移
.go_on_skip:
    mov cl,[put_int_buffer+edi]
    inc edi
    cmp cl,'0'                  ;判断下一位字符是否为0
    je .skip_prefix_0
    dec edi                     ;若当前字符不为'0',则使edi减1恢复当前字符
    jmp .put_each_num           ;若下一位不为0,则从这一位开始遍历
.full0:
    mov cl,'0'                  ;当输入字符都是0时,只打印0
.put_each_num:
    push ecx                    ;此时ecx中为可打印字符,作为参数传递入put_char中
    call put_char
    add esp,4                   ;覆盖掉ecx,清理栈参数,相当于pop ecx
    inc edi                     ;使edi指向下个字符
    mov cl,[put_int_buffer+edi] ;将下个字符放入cl中
    cmp edi,8
    jl .put_each_num
    popad
    ret

 

 

2.代码解释

put_int 函数体只有 60 行左右。该函数的功能是将 32位整型数字转换成字符后输出。函数转换实现的原理是按十六进制来处理32位数字,每4位二进制表示1位十六进制,将各十六进制数字转换成对应的字符,一共8个十六进制数字要处理。

用 section 定义了一个数据区,里面用伪指令 dq 申请了 8 字节的内存put_int_buffer,它作为转换过程中用的缓冲区,实际上它的用途就是用于存储转换后的字符。

这个缓冲区是 8 字节大小。因为我们只支持 32 位数字的输出,按每 4 位二进制数为1 位十六进制数计算,共8个十六进制数字要处理,每个数字虽然只是4位,但它们转换成对应的字符后,这些数字就得变成对应的ASCII码,ASCII码是1字节大小,所以每个字符需要1字节的空间,这就是需要8字节缓冲区的原因。说一下伪指令dq (它是由编译器提供的,并不是CPU支持的指令,所以称之伪指令),它用来定义操作数占用的字节数,q是quad的简写,意为4,定义4个字,也就是8个字节。

section .data
put_int_buffer dq 0             ;定义8字节缓冲区用于数字到字符的转换

 

put_int先是备份寄存器环境,这里还是用pushad指令备份全部 32位通用寄存器。这里多说两句,与之前不同的是这里借鉴C调用约定,先把栈顶esp赋值给ebp,再通过ebp来获取参数。这是32位保护模式下的改进,即内存寻址中支持用esp作为基址。

之前我们通过esp直接从栈中获取,通过那样的例子知道函数中”push ebp”, “mov ebp, esp”, “mov xx,[ebp+n]”用这三步获取参数并不是必须的。不过话又说回来,直接通过esp获取参数是不太好的习惯,难免有压栈操作会改变esp,用ebp就不同了,不显式改变它永远不会变。所以,尽管32位支持寄esp寻址,但还是建议通过ebp来获取参数,以后我们也会这样做。

在将参数获取到寄存器 eax 后,又将其送到 edx 中,这两个寄存器在后面要配合在一起使用。eax 寄存器是作为参数的备份,而edx寄存器是每次参与数位转换的寄存器,主要是由它做转换源,每当转换完1个十六进制数后,再由eax为其更新下一个待转换的数字。

put_int:
    pushad
    mov ebp,esp
    mov eax,[ebp+4*9]           ;将参数写入eax中,call返回地址占4B+pushad的8个4B
    mov edx,eax                 ;eax存储的是参数的备份,edx为每次参与位变换的参数,当转换为16进制数字后,eax将下一个参数给edx

 

为edi赋值为 7,它表示在缓冲区中的偏移量,这里偏移为 7,表示指向缓冲区中最后一个1字节,目的是在该地址处存储数字最低 4 位二进制(也就是十六进制中的最低 1 位)对应的字符。

mov edi,7

 

为ecx赋值为8,它表示要处理的数字的个数,32位数字中,每4位二进制表示1个十六进制,十六进制数字的个数是8,所以共8个。

mov ecx,8

 

把ebx作为缓冲区的基址,该地址就是前面已经定义的put_int_buffer,我们把数字转换后的字符都存储在这里面。

mov ebx,put_int_buffer      ;ebx为缓冲区的基址

 

.16based_4bits便是 put_int的核心,将32位数字按照十六进制的形式从低位到高位逐个处理。and edx,0x0000_000F通过and与运算只保留数字的最低十六进制位,这是我们最先处理的部分,也就是先从32 位数字的最低4 位开始处理。接下来要将它转换成对应的字符。

cmp edx,9开始判断该数字是否大于9,大于9则属于 A~F 范围,否则属于 0~9。 接下来便是按照以上规则分别转换字符,对于字符0’和字符’A’的ASCII码,直接写字符0’和字符’A’,由编译器将其转换成各自的ASCII码。 这时候咱们的缓冲区 put_int_buffer 派上用场了,字符转换完成后就存储到这里。 在由数字变成字符后,它在内存中的顺序可不能按照各位数字本身在内存中的顺序了(x86架构是小端字符序,数字中的高位在内存的高地址,数字中的低位在内存的低地址),因为它们已经不再是数字了,处理它们的时候,都是以各字符(1字节)的单位来处理的,所以,最好按照正常顺序来存储,高位的字符放在前面低地址,低位的字符放在后面高地址,这才是人习惯的自然顺序。 不过这样一来,其存储顺序就有些类似大端字节序了,在.store:开始存储字符,edi之前已经被赋值为7,这是从后往前写字符,通过”mov [ebx+edi], dl”往put_int_buffer中写入转换好的字符,dec指令使寄存器edi的值逐渐减少,使偏移量由高到低。 shr eax,4通过 shr 右移指令把 eax 寄存器向右移动 4 位,去掉已转换完成的低 4 位,随后赋值给edx,再进行下一轮的处理。 这就是前面所说的eax和edx的“配合”。

;将32位数字按照16进制的形式从低到高逐个处理,共处理8个16进制数字
.16based_4bits:
    ;将32位数字按照16进制形式从低到高逐字处理
    and edx,0x0000_000F          ;解析16进制数字的每一位,and后edx只有低4位有效(最低位的16进制数字)
    cmp edx,9                   ;数字0~9和a~f需要分别处理成对应的字符
    jg .is_A2F                  ;jg:Jump if Greater,若大于9,则跳转.is_A2F
    add edx,'0'                 ;如果是0~9,则加上'0'的ASCII码
    jmp .store
.is_A2F:
    sub edx,10                  ;A~F减去10所得的差,10的ASCII码为1
    add edx,'A'                 ;加上10的ASCII码得到字符的ASCII码
;将每个数字转换成对应的字符后,按照类似大端的顺序存储到缓冲区put_int_buffer中。
;高位字符放在低地址,低位字符放在高地址,这样和大端字符序类似。
.store:
    ;此时dl中应该是对应数字的ASCII码
    mov [ebx+edi],dl
    dec edi
    shr eax,4                   ;右移4位,去掉最低4位
    mov edx,eax
    loop .16based_4bits
;现在把put_int_buffer中已全是字符,打印之前把高位连续的字符去掉。
;例如:000123 -> 123

 

准备跳过原来数字中高位的0。如果待打印的数字其高位为0,比如0x00012345这样的形式,我们在打印的时候应该将前面连续的多个0去掉,仅输出为0x12345才更人性化,注意,打印到屏幕上的字符不包括十六进制的0x前缀。

inc edi,edi作为缓冲区中的偏移量,经过前面的转换之后,edi此时已经为-1 了,即0xffffffff,故通过inc指令使其恢复为0,这也是为指向缓冲区中最低地址做准备。

然后便是从最高位字符逐个与字符0比对,直到找出不为0的字符。 寄存器cl用来存储字符ASCII码,表示当前处理的字符,edi作为字符指针,用来指向缓冲区中的字符地址。 其中,mov cl,[put_int_buffer+edi]由于缓冲区中的字符已经是按照“大端字符序”存储了,所以此时的edi作为偏移量,其值为0,缓冲区中的偏移从0起便指向最高位字符。

inc edi将edi用dec指令减1,如果发现当前字符为非’0’时,edi通过inc指令已经指向了下一个字符,为了使后面的打印方便,当前字符cl和字符指针edi应该是匹配的,所以在此将edi恢复成指向当前字符。如果在最高位找到字符’0’,程序流程会到. skip_prefix_0,在这里会判断数字字符是否为全0,这里的逻辑是偏移 edi变成 8 时,表示已经找到了 8 个 0,所以判断为全 0。 比如数字为0x00000000这种情况,其转换的字符会是”0″0″0″0″0″0″0″0″。 随后会跳到.full0处。

.ready_to_print:
    inc edi                     ;此时edi为-1(0xffff_ffff),加1使其为0
.skip_prefix_0:
    cmp edi,8                   ;若以及比较到第9个字符,表示待打印的字符都是0
    je .full0                   ;Jump if Equal
;找出连续的0字符,edi作为非0的最高位字符的偏移
.go_on_skip:
    mov cl,[put_int_buffer+edi]
    inc edi
    cmp cl,'0'                  ;判断下一位字符是否为0
    je .skip_prefix_0

 

.full0:在那里将其处理为字符’0’。 程序执行到此,此时的 edi 指向缓冲区中左起第 1个非’0’的字符,接下来.put_each_num:会逐个打印后面的字符,这样就实现了打印字符串的目的。 最后用popad指令恢复32位寄存器环境后在第217行结束返回。

.full0:
    mov cl,'0'                  ;当输入字符都是0时,只打印0
.put_each_num:
    push ecx                    ;此时ecx中为可打印字符,作为参数传递入put_char中
    call put_char
    add esp,4                   ;覆盖掉ecx,清理栈参数,相当于pop ecx
    inc edi                     ;使edi指向下个字符
    mov cl,[put_int_buffer+edi] ;将下个字符放入cl中
    cmp edi,8
    jl .put_each_num
    popad
    ret

 

同样修改文件 print.h 和 main.c

print.h

#ifndef __LIB_KERNEL_PRINT_H    //防止头文件被重复包含
#define __LIB_KERNEL_PRINT_H    //以print.h所在路径定义了这个宏,以该宏来判断是否重复包含
#include "stdint.h"
void put_char(uint8_t char_asci);
void put_str(char* message);
void put_int(uint32_t num);     //以16进制打印
#endif

main.c

#include "print.h"
void main(void){
    put_str("I am kernel\n");
    put_int(0);
    put_char('\n');
    put_int(9);
    put_char('\n');
    put_int(0x00021a3f);
    put_char('\n');
    put_int(0x12345678);
    put_char('\n');
    put_int(0x00000000);
    while (1);
}

 

编译链接后写入硬盘,运行结果如下:

 

3.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇