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 $-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

- 在汇编代码中导出符号供外部引用是用的关键字 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:
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;
}
运行结果:

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

被折叠的 条评论
为什么被折叠?



