最新消息:

写操作系统之实现进程

博客园首页 William 1浏览 0评论

C语言和汇编语言混合编程

方法

本节的“混合编程”不是指在C语言中使用汇编语言,或在汇编语言中使用C语言。它是指在C程序中使用汇编语言编写的函数、变量等,或者反过来。

混合编程的核心技巧是两个关键字:externglobal

有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会调用exitwrite的编号是4。

exit和write的函数原型如下。

void exit(int status);
int write(int handle, void *buf, int nbyte);

ebxecxedx中的值分别是系统调用函数的第一个参数、第二个参数、第三个参数。

编译运行

用下面的命令编译然后运行。

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语言源代码中更方便地使用这些数据。

切换堆栈,除了使用更方便,还为了修改堆栈的地址。

切换堆栈的理由,我也不是特别明白这样做的重要性。

怎么做

  1. 汇编代码文件kernel.asm,C语言代码文件main.c。
  2. 在C语言代码中定义变量gdt_ptr
  3. 在kernel.asm中导入gdt_ptr
  4. 在kernel.asm中使用sgdt [gdt_ptr]把寄存器gdtptr中的数据保存到变量gdt_ptr中。
  5. 在main.c中把GDT复制到main.c中的新变量中,并且修改GDT的界限。
  6. 在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中。

这段代码非常考验对指针的掌握程度。我们一起来看看。

  1. gdt_ptr[2]是gdt_ptr的第3个字节的数据。
  2. &gdt_ptr[2]是gdt_ptr的存储第3个字节的数据的内存空间的内存地址。
  3. (int *)(&gdt_ptr[2])),把内存地址的数据类型强制转换成int *。一个内存地址,只能确定是一个指针类型,但不能确定是指向哪种数据的指针。强制转换内存地址的数据类型为int *后,就明确告知编译器这是一个指向int数据的指针。
  4. 指向int数据的指针意味着什么?指针指向的数据占用4个字节。
  5. (*((int *)(&gdt_ptr[2])))是指针(int *)(&gdt_ptr[2]))指向的内存空间(4个字节的内存空间)中的值。从寄存器gdtptr的数据结构看,这个值是GDT的基地址,也是一个内存地址。
  6. Memcpy的函数原型是void Memcpy(void *dst, void *src, int size);,第一个参数dst的类型是void *,是一个内存地址。(void *)(*((int *)(&gdt_ptr[2])))中最外层的(void *)把内存地址强制转换成了void *类型。
  7. gdt_ptr[0])是gdt_ptr的第1个字节的数据,&gdt_ptr[0])是存储gdt_ptr的第1个字节的数据的内存空间的内存地址。
  8. (short *)(&gdt_ptr[0]),把内存地址的数据类型强制转换成short *short *ptr这种类型的指针指向两个字节的内存空间。假如,ptr的值是0x01,那么,short *ptr指向的内存空间是内存地址为0x010x02的两个字节。
  9. 说得再透彻一些。short *ptr,指向一片内存空间,ptr的值是这片内存空间的初始地址,而short *告知这片内存空间的长度。short *表示这片内存空间有2个字节,int *表示这片内存空间有4个字节,char *表示这片内存空间有1个字节。
  10. *((short *)(&gdt_ptr[0]))是内存空间中的值,是gdt_ptr[0]、gdt_ptr[1]两个字节中存储的数据。从gdtptr的数据结构来看,这两个字节中存储的是GDT的界限。
  11. GDT的长度 = GDT的界限 + 1。
  12. 现在应该能理解Memcpy这条语句了。从GDT的基地址开始,复制GDT长度那么长的数据到变量gdt中。
  13. gdt是C代码中存储GDT的变量,它的数据类型是Descriptor [128]
  14. Descriptor是表示描述符的结构体。gdt是包含128个描述符的数组。这符合GDT的定义。
  15. 把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;
  1. 由于GDT已经被保存到了新的内存空间中,以后将使用这片内存中的GDT,所以,需要更新寄存器gdtptr中存储的GDT的基地址和GDT界限。
  2. 使用lgdt [gdt_ptr]更新gdtptr中的值。要更新gdtptr中的值,需要先更新gdt_ptr中的值。
  3. 更新gdt_ptr的过程,又是玩耍指针的过程。熟悉指针的读者,能轻松看懂这几条语句,不需要我啰里啰嗦的讲解。
  4. 在前面,我说过,指针,表示这个变量的值是一个内存地址;而指针的类型(或者说指针指向的数据的数据类型)告知从这个内存地址开始有多少个字节的数据。再说得简单一些:指针,告知内存的初始地址;指针的类型,告知内存的长度。
  5. pm_gdt_limit是一个short *指针。它的含义是是一段初始地址是&gdt_ptr[0]、长度是2个字节的内存空间。这是什么?联想到寄存器gdtptr的数据结构,它是GDT的界限。
  6. 用通用的方法理解pm_gdt_base。它是GDT的基地址。
  7. 新的GDT表中有128个描述符,相应地,新GDT的界限是128个描述符*一个描述符的长度-1
  8. 更新gdt_ptr的前2个字节的数据的语句是*pm_gdt_limit = 128 * sizeof(Descriptor) - 1
  9. 如果理解这种语句有困难,就理解它的等价语句:mov [eax], 54
  10. 新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个字节,几乎每个字节都有不同的含义。要完全弄明白它很费时间。我只掌握下面这些。

  1. 在bochs中查看eflags的值,使用info eflags。对查看到的结果,大写的位表示为1,小写的表示为0;如:SF 表示1,zf 表示0。
  2. 使用popfdpopf能更新eflags的值。
    1. pof,将栈顶弹入 EFLAGS 的低 16 位。
    2. popfd,将栈顶弹入 EFLAGS的全部空间。
  3. 算术运算和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学习分享网 » 写操作系统之实现进程

文章评论已关闭!