《真象还原》读书笔记——第六章 完善内核

本文详细介绍了C语言和汇编语言的混合编程,重点讨论了stdcall和cdecl两种调用约定,以及如何在C和汇编之间传递参数、实现字符串和整数打印,以及显卡端口控制的相关内容。

6.1 函数调用约定简介

接下来要使用C语言和汇编的混合编程

6.1.1 调用约定

  • 参数的传递方式
  • 参数的传递顺序
  • 是调用者保存寄存器环境还是被调用者保存环境。保存的有哪些寄存器。

我们可以将参数保存到栈中
这时候有出现问题:

  1. 由谁来负责回收。
  2. 参数多的情况下,主调函数按照什么顺序传递。

例如:

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的调用约定
  1. 调用者将所以参数从右往左入栈
  2. 被调用者清理参数所占的栈空间
//主调用者
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 调用约定 我们要用到的
  1. 从左往右入栈
  2. 调用者清理空间
//主调用者
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 $-str

section .text
extern c_print
global	_start		;导出为全局符号,为了给链接器使用
_start:
;;;;;;;;;;;;;;;;;;;调用C代码中的函数c_print;;;;;;;;;;;;;;
	push str
	call c_print
	add esp,4
;;;;;;;;;;;;;;;;;;;;;;退出程序;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	mov eax,1
	int 0x80

global asm_print
asm_print:
	push ebp
	mov ebp,esp
	mov eax,4
	mov ebx,1
	mov ecx,[ebp+8]
	mov edx,[ebp+12]
	int 0x80
	pop ebp
	ret

汇编代码和C代码相互调用

  • 在汇编代码中导出符号供外部引用是用的关键字 global,引用的外部文件的符号是用的关键字extern。
  • 在C代码中只要将符号定义为全局便可以被外部引用,引用外部符号时候使用extern声明即可。

6.3 实现自己的打印函数

6.3.1 显卡的端口控制

这里按照它们在图形线管(位于CPU和video之间)中的位置的顺序。
VGA寄存器
计算机工程师把每一个寄存器分组视为一个寄存器数组,提供一个寄存器用于指定数组的下标,再提供一个寄存器用于索引所指向的数组的元素(也就是寄存器)进行输入输出操作。
这两个寄存器就是各组中的 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:
    pushad
    mov ax,SELECTOR_VIDEO
    mov gs,ax

    ;;;;;;;;;;获取当前光标位置;;;;;;;;;
    ;8位
    mov dx,0x03d4
    mov al,0x0e
    out dx,al
    mov dx,0x03d5
    in al,dx
    mov ah,al

    ;8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    in al,dx
    
    ;光标放入 bx
    mov bx,ax
    ;放入待打印的字符
    mov ecx,[esp + 36] ;因为pushad压入4*8=32字节,还要跳过4字节的返回地址。
    cmp cl,0xd ;回车符号
    jz .is_carriage_return 
    cmp cl,0xa ;换行符号
    jz .is_line_feed

    cmp cl,0x8 ;Backspace的ascll码是0x8
    jz .is_backspace
    jmp .put_other

.is_backspace:
    dec bx
    shl bx,1 ;坐标左移1,并将等待删除的字节补上0或空格
    mov byte [gs:bx],0x20
    inc bx
    mov byte [gs:bx],0x07
    shr bx,1
    jmp .set_cursor

.put_other:
    shl bx,1
    mov [gs:bx],cl
    inc bx
    mov byte [gs:bx],0x07
    shr bx,1
    inc bx
    cmp bx,2000
    jl .set_cursor

.is_line_feed:          ;换行符 LF(\n)
.is_carriage_return:    ;回车符 CR(\r)
    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:      ;若是LF(\n),将光标移+80即可
    jl .set_cursor

;滚屏,把屏幕的 1-24行搬运到第 0-23;再将第 24 行用空格填充。
.roll_screen:
    cld
    mov ecx,960
    mov esi,0xc00b80a0;第一行首部
    mov edi,0xc00b8000;第零行首部
    rep movsd

    ;空白填充最后一行
    mov ebx,3840
    mov ecx,80

.cls:
    mov word [gs:ebx], 0x0720;黑底白字空格键
    add ebx,2
    loop .cls
    mov bx,1920

.set_cursor:
;设置光标为 bx 值
;8位
    mov dx,0x03d4
    mov al,0x0e
    out dx,al
    mov dx,0x03d5
    mov al,bh
    out dx,al
;8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    mov al,bl
    out dx,al
.put_char_done:
    popad
    ret


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.asm

clang -I lib/ -m32 -s -w -c -o main.o main.c

ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o lib/kernel/print.o

dd 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 ebx
    push ecx
    xor ecx,ecx
    mov ebx, [esp+12]
.goon:
    mov cl,[ebx]
    cmp cl,0
    jz .str_over
    push ecx
    call put_char
    add esp,4
    inc ebx
    jmp .goon
.str_over:
    pop ecx
    pop ebx
    ret

;----------------- put_char ------------------
;功能描述:把栈中一个字符写入光标处
;---------------------------------------------
global put_char
put_char:
    pushad
    mov ax,SELECTOR_VIDEO
    mov gs,ax

    ;;;;;;;;;;获取当前光标位置;;;;;;;;;
    ;8位
    mov dx,0x03d4
    mov al,0x0e
    out dx,al
    mov dx,0x03d5
    in al,dx
    mov ah,al

    ;8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    in al,dx
    
    ;光标放入 bx
    mov bx,ax
    ;放入待打印的字符
    mov ecx,[esp + 36] ;因为pushad压入4*8=32字节,还要跳过4字节的返回地址。
    cmp cl,0xd ;回车符号
    jz .is_carriage_return 
    cmp cl,0xa ;换行符号
    jz .is_line_feed

    cmp cl,0x8 ;Backspace的ascll码是0x8
    jz .is_backspace
    jmp .put_other

.is_backspace:
    dec bx
    shl bx,1 ;坐标左移1,并将等待删除的字节补上0或空格
    mov byte [gs:bx],0x20
    inc bx
    mov byte [gs:bx],0x07
    shr bx,1
    jmp .set_cursor

.put_other:
    shl bx,1
    mov [gs:bx],cl
    inc bx
    mov byte [gs:bx],0x07
    shr bx,1
    inc bx
    cmp bx,2000
    jl .set_cursor

.is_line_feed:          ;换行符 LF(\n)
.is_carriage_return:    ;回车符 CR(\r)
    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:      ;若是LF(\n),将光标移+80即可
    jl .set_cursor

;滚屏,把屏幕的 1-24行搬运到第 0-23;再将第 24 行用空格填充。
.roll_screen:
    cld
    mov ecx,960
    mov esi,0xc00b80a0;第一行首部
    mov edi,0xc00b8000;第零行首部
    rep movsd

    ;空白填充最后一行
    mov ebx,3840
    mov ecx,80

.cls:
    mov word [gs:ebx], 0x0720;黑底白字空格键
    add ebx,2
    loop .cls
    mov bx,1920

.set_cursor:
;设置光标为 bx 值
;8位
    mov dx,0x03d4
    mov al,0x0e
    out dx,al
    mov dx,0x03d5
    mov al,bh
    out dx,al
;8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    mov al,bl
    out dx,al
.put_char_done:
    popad
    ret


补充 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.asm
	nasm -I inc/ -o loader.bin loader.asm

mbr.bin:mbr.asm
	nasm -I inc/ -o mbr.bin mbr.asm

kernel.bin:main.c
	nasm -I lib/kernel -f elf -o lib/kernel/print.o lib/kernel/print.asm
	clang -I lib/kernel -m32 -s -w -c -o main.o main.c
	ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o lib/kernel/print.o


dd: dd_mbr dd_loader dd_kernel
dd_mbr:
	dd if=mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc

dd_loader:
	dd if=loader.bin of=hd60M.img bs=512 count=4 seek=2 conv=notrunc

dd_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 ebx
    push ecx
    xor ecx,ecx
    mov ebx, [esp+12]
.goon:
    mov cl,[ebx]
    cmp cl,0
    jz .str_over
    push ecx
    call put_char
    add esp,4
    inc ebx
    jmp .goon
.str_over:
    pop ecx
    pop ebx
    ret

;----------------- put_char ------------------
;功能描述:把栈中一个字符写入光标处
;---------------------------------------------
global put_char
put_char:
    pushad
    mov ax,SELECTOR_VIDEO
    mov gs,ax

    ;;;;;;;;;;获取当前光标位置;;;;;;;;;
    ;高8位
    mov dx,0x03d4
    mov al,0x0e
    out dx,al
    mov dx,0x03d5
    in al,dx
    mov ah,al

    ;低8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    in al,dx
    
    ;光标放入 bx
    mov bx,ax
    ;放入待打印的字符
    mov ecx,[esp + 36] ;因为pushad压入4*8=32字节,还要跳过4字节的返回地址。
    cmp cl,0xd ;回车符号
    jz .is_carriage_return 
    cmp cl,0xa ;换行符号
    jz .is_line_feed

    cmp cl,0x8 ;Backspace的ascll码是0x8
    jz .is_backspace
    jmp .put_other

.is_backspace:
    dec bx
    shl bx,1 ;坐标左移1位,并将等待删除的字节补上0或空格
    mov byte [gs:bx],0x20
    inc bx
    mov byte [gs:bx],0x07
    shr bx,1
    jmp .set_cursor

.put_other:
    shl bx,1
    mov [gs:bx],cl
    inc bx
    mov byte [gs:bx],0x07
    shr bx,1
    inc bx
    cmp bx,2000
    jl .set_cursor

.is_line_feed:          ;换行符 LF(\n)
.is_carriage_return:    ;回车符 CR(\r)
    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:      ;若是LF(\n),将光标移+80即可
    jl .set_cursor

;滚屏,把屏幕的 1-24行搬运到第 0-23 行
;再将第 24 行用空格填充。
.roll_screen:
    cld
    mov ecx,960
    mov esi,0xc00b80a0;第一行首部
    mov edi,0xc00b8000;第零行首部
    rep movsd

    ;空白填充最后一行
    mov ebx,3840
    mov ecx,80

.cls:
    mov word [gs:ebx], 0x0720;黑底白字空格键
    add ebx,2
    loop .cls
    mov bx,1920

.set_cursor:
;设置光标为 bx 值
;高8位
    mov dx,0x03d4
    mov al,0x0e
    out dx,al
    mov dx,0x03d5
    mov al,bh
    out dx,al
;低8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    mov al,bl
    out dx,al
.put_char_done:
    popad
    ret

;------------------ 将小端字节序数字编程对于的ASCII ,倒置------
;输入:栈中参数为待打印的数字
;输出:在屏幕打印16进制数字,并不会打印前缀 0x
;例如 打印 15(10) 时候 打印的为 f
;------------------------------------------------------------
global put_int
put_int:
    pushad
    mov ebp,esp
    mov eax,[ebp+4*9]
    mov edx,eax
    mov edi,7
    mov ecx,8
    mov ebx,put_int_buffer
;将32位数字按照16进制的形式从低到高位逐个处理
;共处理8个十六进制数字
.16based_4bits:
    and edx,0x0000000F
    cmp edx,9
    jg .is_A2F
    add edx,'0'
    jmp .store 
.is_A2F:
    sub edx,10
    add 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,8
    je .full0
.go_on_skip:
    mov cl,[put_int_buffer+edi]
    inc edi 
    cmp cl,'0'
    je .skip_prefix_0;判断继续下一个字符是否是字符0
    dec edi 
    jmp .put_each_num 

.full0:
    mov cl,'0'
.put_each_num:
    push ecx
    call put_char 
    add esp,4 
    inc edi 
    mov cl,[put_int_buffer+edi]
    cmp edi,8
    jl .put_each_num
    popad 
    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;
}

运行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值