Loading... 昨天在看特权级相关的东西,看的云里雾里,没搞得很懂,考虑到短期之内不会弄得特别深,而且我们也用不上调用门,相关的较复杂的问题也应该不会碰到,所以准备暂时跳过。 ### 调用约定 在说 sys_write 之前应该先说一下调用约定,我们的操作系统会使用 cdecl。由于我是打 PWN 的,对此调用约定相对还算熟悉,但是还是有学到新的东西 * cdecl 是由主调函数清理栈空间的,即调用压入的参数对栈产生的影响由主调函数消除 * cdecl 下,ecx,edx 两寄存器会被被调函数使用,需要有用户备份其值,eax 保存返回值,除此 3 个寄存器外的寄存器在被调函数返回时都会恢复原值。 以上是32位 C 程序默认使用的调用约定两个特点。关于调用约定其他的细节这里不再赘述。 在进行系统调用时,往往不遵守 cdecl 约定,Linux 下的调用约定为 32 位:eax 存储调用功能号,参数按顺序存于 ebx,ecx,edx,esi,edi,ebp 中。 64 位:eax 存储调用功能号,参数按顺序存于 rdi,rsi,rdx,r10,r8,r9 中。 ### sys_putchar 这是我们操作系统向屏幕输出的最基本函数,别的输出函数基本都是对这个函数的封装。 sys_putchar 是一个内核态函数,用户的特权级无法使用,也不会通过系统调用的方式提供给用户(DPL 为 0)。为了调用方便,我们考虑使用 cdecl 调用约定,即通过栈传参。 该函数需要处理的问题如下: * 处理 LF,CR,BS 三种控制字符 * 输出其余字符,并设置好属性 * 对于输出超过当前屏幕的情况,处理好滚屏 #### 获取光标地址 为了输出,我们需要获得当前显示器的光标位置,这需要和显示适配器进行交互,我觉得深究和什么端口交互之类的问题和学习操作系统关系不大,这里也就不再深究。只要知道由于显示器使用到的寄存器过多,将寄存器进行了分组,我们要用到的就是 CRT Controller Registers 这组寄存器,默认情况下占用的端口为 0x3D4。通过向该端口 in 数据可以选定使用该组中的特定寄存器 获取光标首先要向 0x3D4 端口写入 0x0E 和 0x0F 分别选定 Cursor Location High Register 和 Cursor Location Low Register,通过 out 把指针的地址高 8 位和低 8 位都读出来。 ``` ; get the current cursor addr (high 8 bits) mov dx,0x3D4 ; Address Reg (base) mov al,0x0E ; Cursor Location High Reg (idx) out dx,al mov dx,0x3D5 ; Data Reg (base) in al,dx ; get the high 8 bits of the cursor addr mov ah,al ; get the current cursor addr (low 8 bits) mov dx,0x3D4 ; Address Reg (base) mov al,0x0F ; Cursor Location Low Reg (idx) out dx,al mov dx,0x3D5 ; Data Reg (base) in al,dx ; get the low 8 bits of the cursor addr ; save the cursor addr to bx mov bx,ax ``` #### 判断字符类型 如果是前文所述的 3 个控制字符之一,那么就进行特殊处理,否则直接输出。由于是 32 位程序,所以传入的参数在 [rsp + 4] 处,不过由于有必要保存寄存器的值,函数开头会执行 `pushad` 将 8 个同样寄存器入栈,所以传入的参数在 [rsp + 36] 处 ``` ; get the char wating to be put mov ecx,[esp + 36] ; 32(backup regs) + 4(return addr) = 36 cmp cl,0x0d ; CR(Carriage Return): 0x0d jz .sys_putchar_CarriageReturn cmp cl,0x0a ; LF(Line Feed): 0x0a jz .sys_putchar_LineFeed cmp cl,0x08 ; BF(BackSpace): 0x08 jz .sys_putchar_BackSpace jmp .sys_putchar_AnyOther ; Any other char ``` #### 处理退格 退格的处理比较简单,将光标退格一位并把光标原先指向的字符替换成空格或者 '\0' 就可以了,字符属性默认(0x7,黑底白字)。这里其实属性和字符一起设置,以 word 为单位会更容易,之后可能会改动。 ``` .sys_putchar_BackSpace: dec bx ; cursor back one step shl bx,1 ; bx<<1 <=> bx * 2 mov byte [gs:bx],0x20 ; fill the delete char with ' ' inc bx mov byte [gs:bx],0x07 ; 00000111b, (default status) shr bx,1 ; bx>>1 ,=> bx // 2 jmp .sys_putchar_SetCursor ``` 注意,我们之前把指针地址存储在了 bx 中,之后的操作都是对 bx 进行的,没有真正改变光标位置,直到子函数 `.sys_putchar_SetCursor` 之后才会同样进行设置。 #### 输出字符 输出字符后需要将光标后移一位,由于光标后移了,就可能会有溢出的情况(输出到页面外),我们的处理为避免溢出,即如果光标指向第 2001 字符,代表下一次输出会溢出,此时向上滚屏一行(也就是不跳转至设置光标,执行之后对换行回车的处理)。 ``` .sys_putchar_AnyOther: shl bx,1 ; bx<<1 mov byte byte[gs:bx],cl ; put the char inc bx mov byte byte[gs:bx],0x07 ; set the statu inc bx ; point to the next char shr bx,1 ; bx>>1 cmp bx,2000 ; bx == 2000, don't jmp, bx < 2000, jmp jl .sys_putchar_SetCursor ; if the cursor overflow the maximum of the ; video memory, do a Line Feed, if not, set ; the new cursor. ``` #### 换行、回车 实际上回车是返回到行首,但是一般都是返回到下一行行首,所以可以和换行等同,这里也把两者等同。 ``` .sys_putchar_LineFeed: .sys_putchar_CarriageReturn: xor dx,dx ; high 16 bits of the number to be div mov bx,bx ; low 16 bits of the number to be div mov si,80 ; diver div si sub bx,dx ; bx = bx - bx % 80 => make the cursor point to the front of the line ; CR done add bx,80 ; dx = dx + 80 => point to the next line ; LF done cmp bx,2000 jl .sys_putchar_SetCursor ``` 此处的对光标的计算方法为 `bx = bx - bx % 80 + 80`,每行有 80 个字,这么处理就是先取得当前的行首,然后跳至下一行行首。这里对末尾的处理看似有问题,也就是从输出字符那里执行过来的话,bx 就会变成 2080,但是实际上没有问题,因为这样的值会造成滚屏,滚完屏后直接置 bx 为 1920。 #### 滚屏 说是滚屏,其实是上移一行。其实显示器中有 Start Address High/Low Register 来维护向屏幕输出的缓存开始地址,通过改变这两个寄存器就可以直接实现滚屏。但是这样做涉及硬件 I/O,在编写和时间上都未必是最优的。而且如果我们不依赖这两个寄存器,就可以完全利用 16KB 显存,实现类似 Linux 的多 TTY。如果很有必要缓存屏幕内容,也可以在内存中缓存,不一定要使用显存。 ``` .sys_putchar_RollOneLine: ; move line 1~24 to the line 0~23 and clear the last line ; move line 1~24 to the line 0~23 mov ecx,960 ; ((2000 - 80) * 2)(byte) / 4 =960(dword) mov esi,0xC00B80A0 ; front of line 1 mov edi,0xC00B8000 ; front of line 0 cld ; increase copy rep movsd ; clear the last line mov ecx,80 ; 80 words (only one word at a time) mov ebx,3840 ; (2000 - 80) * 2 = 3840 .sys_putchar_RollOneLine_CLL: mov word [gs:ebx],0x0720 ; blank add ebx,2 loop .sys_putchar_RollOneLine_CLL mov bx,1920 ; make cursor point to the last line ``` 利用 movsd 指令可以很容易地实现上滚。然后清空最后一行(全部置为空格)。再设置光标位置为最后一行行首(1920)。 #### 写回光标 ``` .sys_putchar_SetCursor: ; set the current cursor addr (high 8 bits) mov dx,0x3D4 ; Address Reg (base) mov al,0x0E ; Cursor Location High Reg (idx) out dx,al mov dx,0x3D5 ; Data Reg (base) mov al,bh out dx,al ; set the high 8 bits of the cursor addr ; set the current cursor addr (low 8 bits) mov dx,0x3D4 ; Address Reg (base) mov al,0x0F ; Cursor Location low Reg (idx) out dx,al mov dx,0x3D5 ; Data Reg (base) mov al,bl out dx,al ; set the low 8 bits of the cursor addr ``` 最后需要写回光标位置。 最后完整的 print.S ``` TI_GDT equ 0 RPL0 equ 0 SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0 [bits 32] section .text ; -------------------- sys_putchar -------------------- ; write one char in stack to the cursor ; -------------------------------------------------- global sys_putchar sys_putchar: pushad ; backup all regs (8 * 4 = 32bytes) mov ax,SELECTOR_VIDEO mov gs,ax ; make sure gs stores the right selector ; get the current cursor addr (high 8 bits) mov dx,0x3D4 ; Address Reg (base) mov al,0x0E ; Cursor Location High Reg (idx) out dx,al mov dx,0x3D5 ; Data Reg (base) in al,dx ; get the high 8 bits of the cursor addr mov ah,al ; get the current cursor addr (low 8 bits) mov dx,0x3D4 ; Address Reg (base) mov al,0x0F ; Cursor Location Low Reg (idx) out dx,al mov dx,0x3D5 ; Data Reg (base) in al,dx ; get the low 8 bits of the cursor addr ; save the cursor addr to bx mov bx,ax ; get the char wating to be put mov ecx,[esp + 36] ; 32(backup regs) + 4(return addr) = 36 cmp cl,0x0d ; CR(Carriage Return): 0x0d jz .sys_putchar_CarriageReturn cmp cl,0x0a ; LF(Line Feed): 0x0a jz .sys_putchar_LineFeed cmp cl,0x08 ; BF(BackSpace): 0x08 jz .sys_putchar_BackSpace jmp .sys_putchar_AnyOther ; Any other char .sys_putchar_BackSpace: dec bx ; cursor back one step shl bx,1 ; bx<<1 <=> bx * 2 mov byte [gs:bx],0x20 ; fill the delete char with ' ' inc bx mov byte [gs:bx],0x07 ; 00000111b, (default black back,withe front) shr bx,1 ; bx>>1 ,=> bx // 2 jmp .sys_putchar_SetCursor .sys_putchar_AnyOther: shl bx,1 ; bx<<1 mov byte byte[gs:bx],cl ; put the char inc bx mov byte byte[gs:bx],0x07 ; set the statu inc bx ; point to the next char shr bx,1 ; bx>>1 cmp bx,2000 ; bx == 2000, don't jmp, bx < 2000, jmp jl .sys_putchar_SetCursor ; if the cursor overflow the maximum of the ; video memory, do a Line Feed, if not, set ; the new cursor. .sys_putchar_LineFeed: .sys_putchar_CarriageReturn: xor dx,dx ; high 16 bits of the number to be div mov bx,bx ; low 16 bits of the number to be div mov si,80 ; diver div si sub bx,dx ; dx = dx - dx % 80 => make the cursor point to the front of the line ; CR done add bx,80 ; dx = dx + 80 => point to the next line ; LF done cmp bx,2000 jl .sys_putchar_SetCursor .sys_putchar_RollOneLine: ; move line 1~24 to the line 0~23 and clear the last line ; move line 1~24 to the line 0~23 mov ecx,960 ; ((2000 - 80) * 2)(byte) / 4 =960(dword) mov esi,0xC00B80A0 ; front of line 1 mov edi,0xC00B8000 ; front of line 0 cld ; increase copy rep movsd ; clear the last line mov ecx,80 ; 80 words (only one word at a time) mov ebx,3840 ; (2000 - 80) * 2 = 3840 .sys_putchar_RollOneLine_CLL: mov word [gs:ebx],0x0720 ; blank add ebx,2 loop .sys_putchar_RollOneLine_CLL mov bx,1920 ; make cursor point to the last line .sys_putchar_SetCursor: ; set the current cursor addr (high 8 bits) mov dx,0x3D4 ; Address Reg (base) mov al,0x0E ; Cursor Location High Reg (idx) out dx,al mov dx,0x3D5 ; Data Reg (base) mov al,bh out dx,al ; set the high 8 bits of the cursor addr ; set the current cursor addr (low 8 bits) mov dx,0x3D4 ; Address Reg (base) mov al,0x0F ; Cursor Location low Reg (idx) out dx,al mov dx,0x3D5 ; Data Reg (base) mov al,bl out dx,al ; set the low 8 bits of the cursor addr popad ; reset the regs ret ; -------------------- end of function sys_putchar -------------------- ``` 可以看到这里又设置了段选择字 gs。这样做的原因涉及用户进程,由于用户进程完全不需要也不能直接访问显存,所以没有必要在用户态下把 gs 当作一个段选择子,在许多操作系统下,gs 都被当作一个额外的寄存器存储一些额外的信息;另一方面操作系统也不需要由用户来设置 gs,所以操作系统默认 gs 的值需要重新加载。(我这里的解释和书上略有差别,多说了一些也少说了一些,不太重要,之后到用户进程的时候就可以完全解释清楚了。) 修改了一下 main.c ```cpp #include "print.h" int _start() { sys_putchar('k'); sys_putchar('e'); sys_putchar('r'); sys_putchar('n'); sys_putchar('e'); sys_putchar('l'); sys_putchar('!'); sys_putchar('\n'); sys_putchar('b'); sys_putchar('a'); sys_putchar('c'); sys_putchar('k'); sys_putchar('s'); sys_putchar('p'); sys_putchar('a'); sys_putchar('c'); sys_putchar('e'); sys_putchar('\b'); sys_putchar('\n'); while(1); return 0; } ``` 现在的效果为 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/05/4135902204.png "></div> 在我补全剩下的一些输出函数前先学了一下 makefile,用脚本构建实在太逗了。 现在我写好了 Makefile,当然由于这个东西比较复杂,我写的还是比较烂的,总之现在是可以 make 一键编译了。 然后我在 print.S 中添加了 sys_putstr 函数 ``` ; -------------------- sys_putstr -------------------- ; write a string (end by '\0') ; ---------------------------------------------------- global sys_putstr sys_putstr: push ecx push ebx mov ebx,[esp + 12] xor ecx,ecx xor eax,eax .sys_putstr_PutNext: mov cl,[ebx] test cl,cl jz .sys_putstr_EndOfStr push ecx call sys_putchar add esp,4 inc ebx inc eax jmp .sys_putstr_PutNext .sys_putstr_EndOfStr: pop ebx pop ecx ret ; -------------------- end of function sys_putstr -------------------- ``` 输出使用 sys_putstr 完成。 修改 main.c 为 ```cpp #include "print.h" int _start() { sys_putstr("this is kernel!\n"); sys_putstr("Back Space\b"); while(1); return 0; } ``` 现在的效果为 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/05/3838936942.png "></div> 书上还实现了一个输出十六进制数的函数,我觉得没有必要用汇编实现这个(太折磨了),完全可以用 C 来写。所以我就不写了。 最后修改:2021 年 05 月 20 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧