C语言和汇编语言混合编程
方法
本节的“混合编程”不是指在C语言中使用汇编语言,或在汇编语言中使用C语言。它是指在C程序中使用汇编语言编写的函数、变量等,或者反过来。
混合编程的核心技巧是两个关键字:extern
和global
。
有A、B两个源码文件,A是C语言源码文件,B是汇编语言源码文件。在B中定义了变量、函数,要用global导出才能在A中使用;在B中要使用A中的变量、函数,要用extern导入。
用最简单的话说:使用extern导入,使用global导出。
在A中使用B中的函数或变量,只需在B中导出。
在B中使用A中的函数或变量,只需在B中导入。
是这样吗?
请看例程。
例程
代码
bar.c是C语言写的源码文件,foo.asm是汇编写的源码文件。在foo.asm中使用bar.c中创建的函数,在bar.c中使用foo.asm提供的函数。foo.asm创建的函数用global
导出,使用bar.c中创建的函数前使用extern
导入。
foo.asm。
extern choose
[section .data]
GreaterNumber equ 51
SmallerNumber equ 23
[section .text]
global _start
global _displayStr
_start:
push GreaterNumber
push SmallerNumber
call choose
add [esp+8] ; 人工清除参数占用的栈空间
; 必须调用 exit,否则会出现错提示,程序能运行。
mov eax, 1 ; exit系统调用号为1
mov ebx, 0 ; 状态码0:正常退出
int 0x80
ret
; _displayStr(char *str, int len)
_displayStr:
mov eax, 4 ; write系统调用号为4
mov ebx, 1 ; 文件描述符1:标准输出stdout
; 按照C函数调用规则,最后一个参数最先入栈,它在栈中的地址最大。
mov ecx, [ebp + 4] ; str
mov edx, [ebp + 8] ; len。ebp + 0 是 cs:ip中的ip
int 0x80
ret ; 一定不能少
bar.c。
void choose(int a, int b)
{
if(a > b){
_displayStr("first", 5);
}else{
_displayStr("second", 6);
}
return;
}
代码讲解
导入导出
extern choose
,把choose
函数导入到foo.asm
中。
global _displayStr
,导出foo.asm
中的函数,提供给其他文件例如bar.c
使用。
系统调用
系统调用模板。
; code-A
mov eax, 4 ; write系统调用号为4
mov ebx, 1 ; 文件描述符1:标准输出stdout
; 按照C函数调用规则,最后一个参数最先入栈,它在栈中的地址最大。
mov ecx, [ebp + 4] ; str
mov edx, [ebp + 8] ; len。ebp + 0 是 cs:ip中的ip
int 0x80
; code-B
; 必须调用 exit,否则会出现错提示,程序能运行。
mov eax, 1 ; exit系统调用号为1
mov ebx, 0 ; 状态码0:正常退出
int 0x80
两段代码展示了系统调用int 0x80
的用法。暂时不必理会“系统调用”这个概念。
使用int 0x80
时,eax
的值是希望执行的系统函数的编号。例如,exit
的编号是1,当eax
中的值是1时,int 0x80
会调用exit
。write
的编号是4。
exit和write的函数原型如下。
void exit(int status);
int write(int handle, void *buf, int nbyte);
ebx
、ecx
、edx
中的值分别是系统调用函数的第一个参数、第二个参数、第三个参数。
编译运行
用下面的命令编译然后运行。
nasm -f elf foo.o foo.asm
gcc -o bar.o bar.c -m32
ld -s -o kernel.bin foo.o bar.o -m elf_i386
# 运行
./kernel.bin
切换堆栈和GDT
是什么
在《开发加载器》中,我们已经完成了一个简单的内核。那个内核是用汇编语言写的,使用的GDT在实模式下创建。在以后的开发中,我们讲主要使用C语言开发。能在C语言中使用汇编语言中的变量,例如GDT,可是,要往GDT中增加一个全局描述符或修改全局描述符的属性,在C语言中,就非常不方便了。到了用C语言编写的内核源代码中,要想方便地修改GDT或其他在汇编中创建的变量,需要把用汇编源码创建的GDT中的数据复制到用C语言创建的变量中来。
切换GDT,简单地说,就是,把汇编源代码中的变量的值复制到C语言源代码中的变量中来。目的是,在C语言源代码中更方便地使用这些数据。
切换堆栈,除了使用更方便,还为了修改堆栈的地址。
切换堆栈的理由,我也不是特别明白这样做的重要性。
怎么做
- 汇编代码文件kernel.asm,C语言代码文件main.c。
- 在C语言代码中定义变量
gdt_ptr
。 - 在kernel.asm中导入
gdt_ptr
。 - 在kernel.asm中使用
sgdt [gdt_ptr]
把寄存器gdtptr
中的数据保存到变量gdt_ptr
中。 - 在main.c中把GDT复制到main.c中的新变量中,并且修改GDT的界限。
- 在kernel.asm中重新加载
gdt_ptr
中的GDT信息到寄存器gdtptr
中。
请在下面的代码中体会上面所写的流程。
代码讲解
sgdt [gdt_ptr]
,把寄存器gdtptr
中的数据保存到外部变量gdt_ptr
中。
寄存器gdtptr
中存储的数据的长度是6个字节,前2个字节存储GDT的界限,后4个字节存储GDT的基地址。在《开发引导器》中详细讲解过这个寄存器的结构,不清楚的读者可以翻翻那篇文章。
切换GDT
void Memcpy(void *dst, void *src, int size);
typedef struct{
unsigned short seg_limit_below;
unsigned short seg_base_below;
unsigned char seg_base_middle;
unsigned char seg_attr1;
unsigned char seg_limit_high_and_attr2;
unsigned char seg_base_high;
}Descriptor;
Descriptor gdt[128];
Memcpy(&gdt,
(void *)(*((int *)(&gdt_ptr[2]))),
*((short *)(&gdt_ptr[0])) + 1
);
把在实模式下建立的GDT复制到新变量gdt中。
这段代码非常考验对指针的掌握程度。我们一起来看看。
gdt_ptr[2]
是gdt_ptr的第3个字节的数据。&gdt_ptr[2]
是gdt_ptr的存储第3个字节的数据的内存空间的内存地址。(int *)(&gdt_ptr[2]))
,把内存地址的数据类型强制转换成int *
。一个内存地址,只能确定是一个指针类型,但不能确定是指向哪种数据的指针。强制转换内存地址的数据类型为int *
后,就明确告知编译器这是一个指向int数据的指针。- 指向int数据的指针意味着什么?指针指向的数据占用4个字节。
(*((int *)(&gdt_ptr[2])))
是指针(int *)(&gdt_ptr[2]))
指向的内存空间(4个字节的内存空间)中的值。从寄存器gdtptr
的数据结构看,这个值是GDT的基地址,也是一个内存地址。Memcpy
的函数原型是void Memcpy(void *dst, void *src, int size);
,第一个参数dst
的类型是void *
,是一个内存地址。(void *)(*((int *)(&gdt_ptr[2])))
中最外层的(void *)
把内存地址强制转换成了void *
类型。gdt_ptr[0])
是gdt_ptr的第1个字节的数据,&gdt_ptr[0])
是存储gdt_ptr的第1个字节的数据的内存空间的内存地址。(short *)(&gdt_ptr[0])
,把内存地址的数据类型强制转换成short *
。short *ptr
这种类型的指针指向两个字节的内存空间。假如,ptr
的值是0x01
,那么,short *ptr
指向的内存空间是内存地址为0x01
、0x02
的两个字节。- 说得再透彻一些。
short *ptr
,指向一片内存空间,ptr
的值是这片内存空间的初始地址,而short *
告知这片内存空间的长度。short *
表示这片内存空间有2个字节,int *
表示这片内存空间有4个字节,char *
表示这片内存空间有1个字节。 *((short *)(&gdt_ptr[0]))
是内存空间中的值,是gdt_ptr[0]、gdt_ptr[1]两个字节中存储的数据。从gdtptr
的数据结构来看,这两个字节中存储的是GDT的界限。- GDT的长度 = GDT的界限 + 1。
- 现在应该能理解
Memcpy
这条语句了。从GDT的基地址开始,复制GDT长度那么长的数据到变量gdt
中。 gdt
是C代码中存储GDT的变量,它的数据类型是Descriptor [128]
。Descriptor
是表示描述符的结构体。gdt
是包含128个描述符的数组。这符合GDT的定义。- 把GDT从汇编代码中的变量或者说某个内存空间复制到gdt所表示的内存空间后,就完成了GDT的切换。
修改gdtptr
先看代码,然后讲解代码。下面的代码紧跟上面的代码。可在本节的开头看全部代码。
short *pm_gdt_limit = (short *)(&gdt_ptr[0]);
int *pm_gdt_base = (int *)(&gdt_ptr[2]);
//*pm_gdt_limit = 128 * sizeof(Descriptor) * 64 - 1;
*pm_gdt_limit = 128 * sizeof(Descriptor) - 1;
*pm_gdt_base = (int)&gdt;
- 由于GDT已经被保存到了新的内存空间中,以后将使用这片内存中的GDT,所以,需要更新寄存器
gdtptr
中存储的GDT的基地址和GDT界限。 - 使用
lgdt [gdt_ptr]
更新gdtptr
中的值。要更新gdtptr
中的值,需要先更新gdt_ptr
中的值。 - 更新
gdt_ptr
的过程,又是玩耍指针的过程。熟悉指针的读者,能轻松看懂这几条语句,不需要我啰里啰嗦的讲解。 - 在前面,我说过,指针,表示这个变量的值是一个内存地址;而指针的类型(或者说指针指向的数据的数据类型)告知从这个内存地址开始有多少个字节的数据。再说得简单一些:指针,告知内存的初始地址;指针的类型,告知内存的长度。
pm_gdt_limit
是一个short *
指针。它的含义是是一段初始地址是&gdt_ptr[0]
、长度是2个字节的内存空间。这是什么?联想到寄存器gdtptr
的数据结构,它是GDT的界限。- 用通用的方法理解
pm_gdt_base
。它是GDT的基地址。 - 新的GDT表中有128个描述符,相应地,新GDT的界限是
128个描述符*一个描述符的长度-1
。 - 更新
gdt_ptr
的前2个字节的数据的语句是*pm_gdt_limit = 128 * sizeof(Descriptor) - 1
。 - 如果理解这种语句有困难,就理解它的等价语句:
mov [eax], 54
。 - 新GDT的基址是多少?就是变量gdt的内存地址。
更新gdt_ptr的值后,在汇编代码中用lgdt [gdt_ptr]
重新加载新GDT到寄存器gdtptr
中。就这样,完成了切换GDT。
切换堆栈
代码讲解
把esp
的中的数据修改为进入内核后的一段内存空间的初始地址,这就是切换堆栈。
切换堆栈的相关代码如下。我们先看代码,然后理解关键语句。
[section .bss]
Stack resb 1024*2
StackTop:
[section .text]
mov esp, StackTop
Stack resb 1024*2
,从Stack
所表示的内存地址开始,把2048个byte设置成0。
StackTop:
是一个标号,表示一段2kb的内存空间的堆栈的栈顶。
mov esp, StackTop
,把堆栈的栈顶设置成StackTop
。
[section .bss]
,表示后面的代码到下一个节标识前都是.bss
节,一般放置未初始化或初始化为0的数据。
[section .text]
,表示后面的代码到下一个标识前都是.text
节,一般放置指令。
在汇编语言写的源文件中,这些节标识是给程序员看的,非要把数据初始化写到.text
节,编译器也能给你正常编译。
push 0
;push 0xFFFFFFFF
popfd
popfd
把当前堆栈栈顶中的值更新到eflags
中。
eflags
eflags是一个寄存器。先看看它的结构图。
不用我多说,看一眼这张图,就知道它有多复杂。
eflags有32个字节,几乎每个字节都有不同的含义。要完全弄明白它很费时间。我只掌握下面这些。
- 在bochs中查看eflags的值,使用
info eflags
。对查看到的结果,大写的位表示为1,小写的表示为0;如:SF 表示1,zf 表示0。 - 使用
popfd
或popf
能更新eflags的值。-
pof
,将栈顶弹入 EFLAGS 的低 16 位。 popfd
,将栈顶弹入 EFLAGS的全部空间。
-
- 算术运算和eflags的状态标志(即CF、PF、AF、ZF、SF)这些有关系。例如,算术运算的结果的最高位进位或错位,CF将被设置成1,反之被设置成0。
下面是在bochs中查看到的eflags中的值。
# code-A
eflags 0x00000002: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af pf cf
<4><9>
&&&
&proc-&gdt&proc-&gdt&
&gdt
&proc-&gdt&proc-&gdt
&
&的运算规则&当成汇编语言中的运算符
&&可以省略&&&
&&可以省略
&&可以省略
&proc-&gdt&proc-&gdt&&
文章来源于互联网:写操作系统之实现进程
转载请注明:AspxHtml学习分享网 » 写操作系统之实现进程