操作系统开发系列,C语言结合汇编开发系统内核

在这个例子中,源代码包含两个文件:foo.asm, 和 bar.c.程序入口在foo.asm
中,程序先从foo.asm中的_start处开始执行,在_start中,调用一个函数叫bar_func,
而bar_func
函数由bar.c模块来实现,而bar.c实现的bar_func函数中,又调用一个来自foo.asm实现的函数,叫foo_print,
两个模块的相互交互如下:

a.我们先来体验一下在Linux下用汇编编程的感觉,见代码

永利澳门游戏网站 1Paste_Image.png

[section .data] ; 数据在此

strHello    db  "Hello, world!", 0Ah
STRLEN      equ $ - strHello

[section .text] ; 代码在此

global _start   ; 我们必须导出 _start 这个入口,以便让链接器识别

_start:
    mov edx, STRLEN
    mov ecx, strHello
    mov ebx, 1
    mov eax, 4      ; sys_write
    int 0x80        ; 系统调用
    mov ebx, 0
    mov eax, 1      ; sys_exit
    int 0x80        ; 系统调用

永利澳门游戏网站,接下来我们看看两个模块的实现,先看看foo.asm:

编译方法:

extern bar_func;[section .data]arg1 dd 3arg2 dd 4[section .text]global _startglobal foo_print_start:mov eax, dword[arg1]push eaxmov eax, dword [arg2]push eaxcall bar_funcadd esp, 8mov ebx,0mov eax, 1int 0x80foo_print:mov edx, [esp + 8]mov ecx, [esp + 4]mov ebx, 1mov eax, 4int 0x80ret

nasm -f elf hello.asm -o hello.o

由于需要调用另一个模块的函数,所以开始先要使用extern
声明,要不然编译时,编译器会报错。由于_start要导出作为整个可执行程序的入口,因此要用global关键字声明,同时,该模块中的foo_print要导出给其他接口使用,所以需要用global声明。

ld -m elf_i386 -s -o hello hello.o

在_start中,在调用bar_func函数前,需要传入参数,C语言的参数传递是通过堆栈实现的,函数如果有多个参数的话,那么最右边的参数先压入堆栈,由于代码中,我们先压入arg1,
然后再压入arg2,所以就相当于以如下方式调用来自C语言模块的接口:

./hello

bar_func(arg2, arg1);

运行结果是打印出Hello, world!

根据C语言的函数调用规则,堆栈的回收由调用者负责,所以在_start中,bar_func调用结束后,需要调整堆栈指针esp,
add esp ,8
将堆栈指针往下移动8字节,这就将开头压入堆栈的两个4字节参数,arg1,arg2从堆栈上删除了。

入口点默认的是_start,我们不但要定义它,而且要通过global这个关键字将它导出,这样链接程序才能找到它。两个系统调用不用深究,因为在我们自己的OS中根本用不到Linux的系统调用。

在理解foo_print前,我们需要看看bar.c的实现:

b.汇编和C同步使用

void foo_print(char* a, int len);int bar_func(int a, int b) { if  { foo_print("the 1st onen", 13); } else { foo_print("the 2nd onen", 13); } return 0;}
; 编译链接方法
; (ld 的‘-s’选项意为“strip all”)
;
; $ nasm -f elf foo.asm -o foo.o
; $ gcc -c bar.c -o bar.o
; $ ld -s hello.o bar.o -o foobar
; $ ./foobar
; the 2nd one
; $

extern choose   ; int choose(int a, int b);

[section .data] ; 数据在此

num1st      dd  3
num2nd      dd  4

[section .text] ; 代码在此

global _start   ; 我们必须导出 _start 这个入口,以便让链接器识别
global myprint  ; 导出这个函数为了让 bar.c 使用

_start:
    push    dword [num2nd]  ; `.
    push    dword [num1st]  ;  |
    call    choose      ;  | choose(num1st, num2nd);
    add esp, 8      ; /

    mov ebx, 0
    mov eax, 1      ; sys_exit
    int 0x80        ; 系统调用

; void myprint(char* msg, int len)
myprint:
    mov edx, [esp + 8]  ; len
    mov ecx, [esp + 4]  ; msg
    mov ebx, 1
    mov eax, 4      ; sys_write
    int 0x80        ; 系统调用
    ret

根据bar.c中,对foo_print的调用方式来看,最右边的参数是13,表示的是第一个输入参数,也就是字符串的长度。这么看来在foo.asm的foo_print中,[esp+8]
对应于第二个参数,也就是上面的13,[esp+4]对应第一个参数,也就是输入的字符串。

1.由于在bar.c中用到函数myprint(),所以要用关键字global将其导出。

mov ebx, 1mov eax, 4int 0x80

2.由于用到本文件外定义的函数choose(),所以要用关键字extern声明。

上面三句实现Linux的一个系统调用,该调用的作用是将ecx寄存器中指向的内存地址中的字符信息打印到屏幕上。

3.不管是myprint()还是choose(),遵循的都是C调用约定,后面的参数先入栈,并由调用者清理堆栈。

发表评论

电子邮件地址不会被公开。 必填项已用*标注