代码、内容参考来自于包括《操作系统真象还原》、《一个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:一个操作系统的实现


