什么是函数调用约定
一言概之:函数调用约定就是调用函数时,如何传参入栈,调用后如何恢复栈的协定。我们实现的过程中使用的是cdecl协定,基本的原则是:
- 参数按从右到左的顺序入栈
- 由调用者清理栈空间
其中第二点带来了一个很明显的好处,那就是可以传入不定长的参数。
实现打印函数
复习下汇编的知识:
in [num], [port] ; 从port端口读出num,in的主体是内存
out [port], [num] ; 向port端口写入num,out的主体是内存
实现字符打印
然后开始写打印函数:
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
[bits 32]
section .text
;------------ put char ----------------
global put_char
put_char:
pushad ; 备份所有寄存器
mov ax, SELECTOR_VIDEO
mov gs, ax
; 获取光标位置
mov dx, 0x03d4 ; 索引号端口
mov al, 0x0e ; 高8位寄存器
out dx, al
mov dx, 0x03d5 ; 数据端口
in al, dx ; 读取高八位到al
mov ah, al
; 低八位同理
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
mov bx, ax ; 将光标存入bx
mov ecx, [esp + 36] ; 获取待打印的字符,其中0-4位是返回地址,4-36是寄存器
cmp cl, 0xd
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed ; 二者都看成换行+回车
cmp cl, 0x8 ; 退格
jz .is_backspace
jmp .put_other
.is_backspace: ; 退格并将退后的字符设置为空
cmp bx, 0
je .set_cursor
dec bx
shl bx, 1 ; 每次读写光标必须乘2,因为每个位置用2字节表示,1字节代表ascii,1字节代表状态
mov word [gs:bx], 0x0720
shr bx, 1
jmp .set_cursor
.put_other:
shl bx, 1
mov [gs:bx], cl
inc bx
mov byte [gs:bx], 0x07
inc bx
shr bx, 1
cmp bx, 2000
jl .set_cursor
.is_line_feed:
.is_carriage_return:
xor dx, dx
mov ax, bx
mov si, 80
div si
sub bx, dx
.is_carriage_return_end:
add bx, 80
cmp bx, 2000
.is_line_feed_end:
jl .set_cursor
; 1. 将1-24行搬到0-23行
.roll_screen:
cld
mov ecx, 960 ; 960 = (2000 - 80) * 2 / 4
mov esi, 0xc00b80a0
mov edi, 0xc00b8000
rep movsd
; 2. 将最后一行填充为空
mov ebx, 3840
mov ecx, 80
.cls:
mov word [gs:ebx], 0x0720
add ebx, 2
loop .cls
mov bx, 1920 ; 将光标重置为最后一行行首
; 将当前光标写回显存
.set_cursor:
; 高八位
mov dx, 0x03d4
mov al, 0x0e
out dx, al
mov dx, 0x03d5
mov al, bh
out dx, al
; 低八位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret
文章看似字数不多,但我在这里卡了快一天了,终于通过一次次调试得出了理想的结果:
哈哈不容易啊不容易……
打印字符串
接下来写打印字符串,尝试自己手撸:
;------------ put str -----------------
global put_str
put_str:
push ebx
push ecx
mov ebx, [esp + 12]
.print_loop:
mov ecx, [ebx]
cmp cl, 0
je .print_end
push ecx
call put_char
pop ecx
add ebx, 1
jmp .print_loop
.print_end:
pop ecx
pop ebx
ret
检查一下,成功!
打印整数
这个是我们大二上的考试题哈,继续手撸(代码后面有汇总):
到此为止,所有三个打印函数已经全部实现!
给出完美版本的print.S:
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
section .data
buffer dq 0
[bits 32]
section .text
;------------ put str -----------------
global put_str
put_str:
push ebx
push ecx
mov ebx, [esp + 12]
.print_loop:
mov ecx, [ebx]
cmp cl, 0
je .print_end
push ecx
call put_char
pop ecx
add ebx, 1
jmp .print_loop
.print_end:
pop ecx
pop ebx
ret
;------------ put char ----------------
global put_char
put_char:
pushad ; 备份所有寄存器
mov ax, SELECTOR_VIDEO
mov gs, ax
; 获取光标位置
mov dx, 0x03d4 ; 索引号端口
mov al, 0x0e ; 高8位寄存器
out dx, al
mov dx, 0x03d5 ; 数据端口
in al, dx ; 读取高八位到al
mov ah, al
; 低八位同理
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
mov bx, ax ; 将光标存入bx
mov ecx, [esp + 36] ; 获取待打印的字符,其中0-4位是返回地址,4-36是寄存器
cmp cl, 0xd
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed ; 二者都看成换行+回车
cmp cl, 0x8 ; 退格
jz .is_backspace
jmp .put_other
.is_backspace: ; 退格并将退后的字符设置为空
cmp bx, 0
je .set_cursor
dec bx
shl bx, 1 ; 每次读写光标必须乘2,因为每个位置用2字节表示,1字节代表ascii,1字节代表状态
mov word [gs:bx], 0x0720
shr bx, 1
jmp .set_cursor
.put_other:
shl bx, 1
mov [gs:bx], cl
inc bx
mov byte [gs:bx], 0x07
inc bx
shr bx, 1
cmp bx, 2000
jl .set_cursor
.is_line_feed:
.is_carriage_return:
xor dx, dx
mov ax, bx
mov si, 80
div si
sub bx, dx
.is_carriage_return_end:
add bx, 80
cmp bx, 2000
.is_line_feed_end:
jl .set_cursor
; 1. 将1-24行搬到0-23行
.roll_screen:
cld
mov ecx, 960 ; 960 = (2000 - 80) * 2 / 4
mov esi, 0xc00b80a0
mov edi, 0xc00b8000
rep movsd
; 2. 将最后一行填充为空
mov ebx, 3840
mov ecx, 80
.cls:
mov word [gs:ebx], 0x0720
add ebx, 2
loop .cls
mov bx, 1920 ; 将光标重置为最后一行行首
; 将当前光标写回显存
.set_cursor:
; 高八位
mov dx, 0x03d4
mov al, 0x0e
out dx, al
mov dx, 0x03d5
mov al, bh
out dx, al
; 低八位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret
;------------put_int------------
global put_int
put_int:
pushad
mov ebp, esp
add ebp, 36
mov eax, [ebp] ; point to the num
mov edi, buffer
add edi, 7 ; move to the last location
mov ecx, 8 ; num of loop
deal_with_num:
mov ebx, eax
and ebx, 0x0000000f
shr eax, 4
cmp bl, 0x9 ; judge num
jg A2F ; if greater than 9, go to A2F
add bl, '0' ; else add '0'
jmp store ; cross A2F
A2F:
sub bl, 10
add bl, 'A'
store:
mov byte [edi], bl ; store it
dec edi ; point to the next location
loop deal_with_num
mov ecx, 8
xor edx, edx
mov edi, buffer
check_if_all_0:
mov bl, [edi + edx]
cmp bl, '0'
jne do_print_num
inc edx ; count num of '0'
loop check_if_all_0
cmp edx, 8
jne do_print_num
print_all_0:
mov ecx, '0'
push ecx
call put_char
pop ecx
jmp put_int_end
do_print_num:
cmp edx, 8
je put_int_end
mov ecx, [edi + edx]
push ecx
call put_char
pop ecx
inc edx
jmp do_print_num
put_int_end:
popad
ret
什么是内联汇编
一句话,在C中嵌入汇编,提升代码的“战斗力”。
总共花了30分钟左右把最后一章过了一遍,第6章正式结束。
结语
原来第五章埋的雷在这一章爆了,导致调试调了快一整天,最后看到bochs中如我心意地打出字符和数字时,真的很开心,原来最简单的一句printf(不完全的),如果全部手写,都需要我花一整天啊。
总的来说,感觉自己对于汇编的知识提升又高了一些,但是还有很长的路要走啊,预计接下来两天把第六章——中断给吃完吧。