6.1 函数调用约定简介
接下来要使用C语言和汇编的混合编程
6.1.1 调用约定
- 参数的传递方式
- 参数的传递顺序
- 是调用者保存寄存器环境还是被调用者保存环境。保存的有哪些寄存器。
我们可以将参数保存到栈中
这时候有出现问题:
- 由谁来负责回收。
- 参数多的情况下,主调函数按照什么顺序传递。
例如:
subtract(int a, int b) //被调用者
{return a - b;
}int sub = subtract(3, 2); //调用者
我们来模拟下栈的情况:
//调用者
push 2
push 3
call subtract// 被调用者
push ebp
mov ebp,esp
mov eax,[ebp+8]
sub eax,[ebp+12]
6.1.1.1 stdcall的调用约定
- 调用者将所以参数从右往左入栈
- 被调用者清理参数所占的栈空间
//主调用者
push 2
push 3
call subtract//被调用者
push ebp
mov ebp,esp
mov eax,[ebp+0x8]
add eax,[ebp+0xc]
mov esp,ebp //归还esp
pop ebp //归还ebp
ret 8
6.1.1.2 cdecl 调用约定 我们要用到的
- 从左往右入栈
- 调用者清理空间
//主调用者
push 2
push 3
call subtract
add esp,8 //这里是回收[清理]栈空间//被调用者
push ebp
mov ebp,esp
mov eax,[ebp+0x8]
add eax,[ebp+0xc]
mov esp,ebp
pop ebp
ret
6.2 汇编语言和C语言混合编程
6.2.1 C库函数和系统调用
两类,
1.混合编程:单独的汇编文件+单独的C语言文件分别编译后成为目标文件后,一起链接成为可执行程序。
2. 内嵌汇编:在C语言中嵌入汇编代码,直接编译成为可执行程序。
6.2.2 汇编语言和C语言共同协作
//C_with_S_c.c
extern void asm_print(char *, int);
void c_print(char *str){int len = 0;while(str[len++]);asm_print(str, len);
}//C_with_S_S.S
section .data
str: db "asm_print says hello world!", 0xa, 0
str_len equ $-strsection .text
extern c_print
global _start ;导出为全局符号,为了给链接器使用
_start:
;;;;;;;;;;;;;;;;;;;调用C代码中的函数c_print;;;;;;;;;;;;;;push strcall c_printadd esp,4
;;;;;;;;;;;;;;;;;;;;;;退出程序;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mov eax,1int 0x80global asm_print
asm_print:push ebpmov ebp,espmov eax,4mov ebx,1mov ecx,[ebp+8]mov edx,[ebp+12]int 0x80pop ebpret
- 在汇编代码中导出符号供外部引用是用的关键字 global,引用的外部文件的符号是用的关键字extern。
- 在C代码中只要将符号定义为全局便可以被外部引用,引用外部符号时候使用extern声明即可。
6.3 实现自己的打印函数
6.3.1 显卡的端口控制
这里按照它们在图形线管(位于CPU和video之间)中的位置的顺序。
计算机工程师把每一个寄存器分组视为一个寄存器数组,提供一个寄存器用于指定数组的下标,再提供一个寄存器用于索引所指向的数组的元素(也就是寄存器)进行输入输出操作。
这两个寄存器就是各组中的 Address Register 和 Data Register。
在Address Register中指定寄存器的索引值,用于确定操作的寄存器是哪个,然后在Data Register寄存器中对所索引的寄存器进行读写操作。
我们使用的显卡操作只用到了 CRT Controller Registers 分组中的寄存器。
CRT Controller Registers 寄存器组中的 Address Register 和 Data Register 的端口地址并不固定,具体值取决于 Miscellaneous Output Register 寄存器中的 Input/Output Address Select 字段。
I/OAS(Input/Output Address Select)
此位用来选择 CRT controller 寄存器组的地址,这里是指 Address Register 和 Data Register 的地址。
当此位为 0 时:
CRT controller 寄存器组的端口地址被设置为 0x3Bx,结合表 6-2,Address Register 和 Data Register 的
端口地址实际值为 3B4h-3B5h。并且为了兼容 monochrome 适配器(显卡),Input Status Register 寄存器的端口地址被设置为 0x3BA。
当此位为 1 时:
CRT controller 寄存器组的端口地址被设置为 0x3Dx,结合表 6-2,Address Register 和 Data Register 的
端口地址实际值为 3D4h-3D5h。并且为了兼容 color/graphics 适配器(显卡),Input Status Register 寄存器的端口地址被设置为 0x3DA。
默认情况下,Miscellaneous Output Register 寄存器的值为 0x67,其他字段不管,咱们只关注这最重要的 I/OAS 位,其值为 1。也就是说:
- CRT controller 寄存器组的 Address Register 的端口地址为 0x3D4,
Data Register 的端口地址 0x3D5。 - Input Status #1Register 寄存器的端口地址被设置为 0x3DA。
- Feature Control register 寄存器的写端口是 0x3DA
6.3.2 实现单个字符打印
lib/stdint.h
#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;
#endif
print.asm
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:pushadmov ax,SELECTOR_VIDEOmov gs,ax;;;;;;;;;;获取当前光标位置;;;;;;;;;;高8位mov dx,0x03d4mov al,0x0eout dx,almov dx,0x03d5in al,dxmov ah,al;低8位mov dx,0x03d4mov al,0x0fout dx,almov dx,0x03d5in al,dx;光标放入 bxmov bx,ax;放入待打印的字符mov ecx,[esp + 36] ;因为pushad压入4*8=32字节,还要跳过4字节的返回地址。cmp cl,0xd ;回车符号jz .is_carriage_return cmp cl,0xa ;换行符号jz .is_line_feedcmp cl,0x8 ;Backspace的ascll码是0x8jz .is_backspacejmp .put_other.is_backspace:dec bxshl bx,1 ;坐标左移1位,并将等待删除的字节补上0或空格mov byte [gs:bx],0x20inc bxmov byte [gs:bx],0x07shr bx,1jmp .set_cursor.put_other:shl bx,1mov [gs:bx],clinc bxmov byte [gs:bx],0x07shr bx,1inc bxcmp bx,2000jl .set_cursor.is_line_feed: ;换行符 LF(\n)
.is_carriage_return: ;回车符 CR(\r)xor dx,dxmov ax,bxmov si,80div sisub bx,dx.is_carriage_return_end:add bx,80cmp bx,2000
.is_line_feed_end: ;若是LF(\n),将光标移+80即可jl .set_cursor;滚屏,把屏幕的 1-24行搬运到第 0-23 行
;再将第 24 行用空格填充。
.roll_screen:cldmov ecx,960mov esi,0xc00b80a0;第一行首部mov edi,0xc00b8000;第零行首部rep movsd;空白填充最后一行mov ebx,3840mov ecx,80.cls:mov word [gs:ebx], 0x0720;黑底白字空格键add ebx,2loop .clsmov bx,1920.set_cursor:
;设置光标为 bx 值
;高8位mov dx,0x03d4mov al,0x0eout dx,almov dx,0x03d5mov al,bhout dx,al
;低8位mov dx,0x03d4mov al,0x0fout dx,almov dx,0x03d5mov al,blout dx,al
.put_char_done:popadret
lib/kernel/print.h
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci);
#endif
编译处理:
nasm -f elf -o lib/kernel/print.o lib/kernel/print.asmclang -I lib/ -m32 -s -w -c -o main.o main.cld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o lib/kernel/print.odd if=kernel.bin of=hd60M.img bs=512 count=200 seek=9 conv=notrunc
运行结果
6.3.3 实现字符串打印
在C语言中,会在字符串末位添加 ‘\0’ ,在这里我们也这么做。
我们会通过判断结尾的 0 来判断字符串是否结束。
lib/kernel/print.asm
添加了 put_str 部分
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0[bits 32]
section .text;------------- put_str ----------------------
global put_str
put_str:push ebxpush ecxxor ecx,ecxmov ebx, [esp+12]
.goon:mov cl,[ebx]cmp cl,0jz .str_overpush ecxcall put_charadd esp,4inc ebxjmp .goon
.str_over:pop ecxpop ebxret;----------------- put_char ------------------
;功能描述:把栈中一个字符写入光标处
;---------------------------------------------
global put_char
put_char:pushadmov ax,SELECTOR_VIDEOmov gs,ax;;;;;;;;;;获取当前光标位置;;;;;;;;;;高8位mov dx,0x03d4mov al,0x0eout dx,almov dx,0x03d5in al,dxmov ah,al;低8位mov dx,0x03d4mov al,0x0fout dx,almov dx,0x03d5in al,dx;光标放入 bxmov bx,ax;放入待打印的字符mov ecx,[esp + 36] ;因为pushad压入4*8=32字节,还要跳过4字节的返回地址。cmp cl,0xd ;回车符号jz .is_carriage_return cmp cl,0xa ;换行符号jz .is_line_feedcmp cl,0x8 ;Backspace的ascll码是0x8jz .is_backspacejmp .put_other.is_backspace:dec bxshl bx,1 ;坐标左移1位,并将等待删除的字节补上0或空格mov byte [gs:bx],0x20inc bxmov byte [gs:bx],0x07shr bx,1jmp .set_cursor.put_other:shl bx,1mov [gs:bx],clinc bxmov byte [gs:bx],0x07shr bx,1inc bxcmp bx,2000jl .set_cursor.is_line_feed: ;换行符 LF(\n)
.is_carriage_return: ;回车符 CR(\r)xor dx,dxmov ax,bxmov si,80div sisub bx,dx.is_carriage_return_end:add bx,80cmp bx,2000
.is_line_feed_end: ;若是LF(\n),将光标移+80即可jl .set_cursor;滚屏,把屏幕的 1-24行搬运到第 0-23 行
;再将第 24 行用空格填充。
.roll_screen:cldmov ecx,960mov esi,0xc00b80a0;第一行首部mov edi,0xc00b8000;第零行首部rep movsd;空白填充最后一行mov ebx,3840mov ecx,80.cls:mov word [gs:ebx], 0x0720;黑底白字空格键add ebx,2loop .clsmov bx,1920.set_cursor:
;设置光标为 bx 值
;高8位mov dx,0x03d4mov al,0x0eout dx,almov dx,0x03d5mov al,bhout dx,al
;低8位mov dx,0x03d4mov al,0x0fout dx,almov dx,0x03d5mov al,blout dx,al
.put_char_done:popadret
补充 print.h 中的put_str
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci);
void put_str(char *message);
#endif
Makefile
loader.bin:loader.asmnasm -I inc/ -o loader.bin loader.asmmbr.bin:mbr.asmnasm -I inc/ -o mbr.bin mbr.asmkernel.bin:main.cnasm -I lib/kernel -f elf -o lib/kernel/print.o lib/kernel/print.asmclang -I lib/kernel -m32 -s -w -c -o main.o main.cld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o lib/kernel/print.odd: dd_mbr dd_loader dd_kernel
dd_mbr:dd if=mbr.bin of=hd60M.img bs=512 count=1 conv=notruncdd_loader:dd if=loader.bin of=hd60M.img bs=512 count=4 seek=2 conv=notruncdd_kernel:dd if=kernel.bin of=hd60M.img bs=512 count=200 seek=9 conv=notrunc
main.c
#include "print.h"
int main(void){put_char('k');put_char('e');put_char('r');put_char('n');put_char('e');put_char('l');put_char('\n');put_char('1');put_char('2');put_char('\b');put_char('3');put_char('\n');put_str("I am kernel\n");put_str("put_str(\"I am kernel\n\");");while(1);return 0;
}
运行结果:
6.3.4 实现整数打印
lib/kernel/print.asm 中添加 put_int
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
[bits 32]section .data
put_int_buffer dq 0
section .text;------------- put_str ----------------------
global put_str
put_str:push ebxpush ecxxor ecx,ecxmov ebx, [esp+12]
.goon:mov cl,[ebx]cmp cl,0jz .str_overpush ecxcall put_charadd esp,4inc ebxjmp .goon
.str_over:pop ecxpop ebxret;----------------- put_char ------------------
;功能描述:把栈中一个字符写入光标处
;---------------------------------------------
global put_char
put_char:pushadmov ax,SELECTOR_VIDEOmov gs,ax;;;;;;;;;;获取当前光标位置;;;;;;;;;;高8位mov dx,0x03d4mov al,0x0eout dx,almov dx,0x03d5in al,dxmov ah,al;低8位mov dx,0x03d4mov al,0x0fout dx,almov dx,0x03d5in al,dx;光标放入 bxmov bx,ax;放入待打印的字符mov ecx,[esp + 36] ;因为pushad压入4*8=32字节,还要跳过4字节的返回地址。cmp cl,0xd ;回车符号jz .is_carriage_return cmp cl,0xa ;换行符号jz .is_line_feedcmp cl,0x8 ;Backspace的ascll码是0x8jz .is_backspacejmp .put_other.is_backspace:dec bxshl bx,1 ;坐标左移1位,并将等待删除的字节补上0或空格mov byte [gs:bx],0x20inc bxmov byte [gs:bx],0x07shr bx,1jmp .set_cursor.put_other:shl bx,1mov [gs:bx],clinc bxmov byte [gs:bx],0x07shr bx,1inc bxcmp bx,2000jl .set_cursor.is_line_feed: ;换行符 LF(\n)
.is_carriage_return: ;回车符 CR(\r)xor dx,dxmov ax,bxmov si,80div sisub bx,dx.is_carriage_return_end:add bx,80cmp bx,2000
.is_line_feed_end: ;若是LF(\n),将光标移+80即可jl .set_cursor;滚屏,把屏幕的 1-24行搬运到第 0-23 行
;再将第 24 行用空格填充。
.roll_screen:cldmov ecx,960mov esi,0xc00b80a0;第一行首部mov edi,0xc00b8000;第零行首部rep movsd;空白填充最后一行mov ebx,3840mov ecx,80.cls:mov word [gs:ebx], 0x0720;黑底白字空格键add ebx,2loop .clsmov bx,1920.set_cursor:
;设置光标为 bx 值
;高8位mov dx,0x03d4mov al,0x0eout dx,almov dx,0x03d5mov al,bhout dx,al
;低8位mov dx,0x03d4mov al,0x0fout dx,almov dx,0x03d5mov al,blout dx,al
.put_char_done:popadret;------------------ 将小端字节序数字编程对于的ASCII ,倒置------
;输入:栈中参数为待打印的数字
;输出:在屏幕打印16进制数字,并不会打印前缀 0x
;例如 打印 15(10) 时候 打印的为 f
;------------------------------------------------------------
global put_int
put_int:pushadmov ebp,espmov eax,[ebp+4*9]mov edx,eaxmov edi,7mov ecx,8mov ebx,put_int_buffer
;将32位数字按照16进制的形式从低到高位逐个处理
;共处理8个十六进制数字
.16based_4bits:and edx,0x0000000Fcmp edx,9jg .is_A2Fadd edx,'0'jmp .store
.is_A2F:sub edx,10add edx,'A'
.store:mov [ebx+edi],dl dec edi shr eax,4 mov edx,eax loop .16based_4bits
.ready_to_print:inc edi
.skip_prefix_0:cmp edi,8je .full0
.go_on_skip:mov cl,[put_int_buffer+edi]inc edi cmp cl,'0'je .skip_prefix_0;判断继续下一个字符是否是字符0dec edi jmp .put_each_num .full0:mov cl,'0'
.put_each_num:push ecxcall put_char add esp,4 inc edi mov cl,[put_int_buffer+edi]cmp edi,8jl .put_each_numpopad ret
补全print.h
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci);
void put_str(char *message);
void put_int(uint32_t num);
#endif
main.c
#include "print.h"
int 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);return 0;
}
运行结果: