学期实践
环境配置
前言
博客教程
环境搭建
ubantu64操作系统
csdn
哈工大李治军老师视频教程
蓝桥云课
第零章 常用汇编指令
cmp
cmp
是一条汇编指令,用于比较两个操作数的值。它通常与条件跳转指令结合使用,以根据比较结果执行不同的操作。在x86架构中,cmp
指令通常用于比较寄存器或内存中存储的值。
以下是cmp
指令的一般语法:
cmp operand1, operand2
其中,operand1
和operand2
可以是寄存器、内存地址或立即数。指令的作用是将operand1
的值减去operand2
的值,并根据结果设置标志寄存器的相应位。具体来说,cmp
指令会进行减法运算,但不会保存结果,只更新标志寄存器。
常用的条件跳转指令(如je
、jne
、jl
、jg
等)可以根据cmp
指令设置的标志位来执行相应的条件跳转操作。
例如,以下示例:
mov ax, 10
cmp ax, 5
je equal
在这个示例中,cmp ax, 5
将比较ax
寄存器中的值和立即数5。如果它们相等,那么je equal
将执行跳转到equal
标签处的代码。
jz
jz
是汇编语言中的一种条件转移指令,它的作用是在”零标志”(ZF)被设置为1时执行跳转。在x86汇编语言中,”零标志”表示最近的运算结果为零。因此,jz
指令通常用于检查上一次操作是否导致结果为零,以决定是否执行跳转。
jz
指令的全称是”jump if zero”,其语法格式为:
jz target
其中,target
是跳转的目标地址。
当执行jz
指令时,处理器会检查标志寄存器中的”零标志”是否为1(也就是ZF位)。如果ZF位为1,则跳转到指定的target
地址;如果ZF位为0,则继续执行下一条指令。
通常,jz
指令与cmp
指令一起使用,以根据比较结果决定是否执行跳转操作,例如:
cmp ax, bx
jz equal
在这个示例中,如果ax
和bx
的值相等(导致ZF被设置为1),则程序将跳转到equal
标签处执行相应的代码。
总之,jz
指令是根据”零标志”的状态来进行条件跳转的一种汇编指令。
基本汇编指令
MOV - 数据传送指令
- 语法:
MOV destination, source
- 功能: 将源操作数的数据传送到目的操作数。
- 示例:
MOV AX, BX ; 将BX寄存器的值传送到AX寄存器 MOV AL, 34h ; 将立即数34h传送到AL寄存器 MOV [1234h], AX ; 将AX寄存器的值传送到内存地址1234h
- 语法:
ADD - 加法指令
- 语法:
ADD destination, source
- 功能: 将源操作数的数据加到目的操作数上,结果存储在目的操作数中。
- 示例:
ADD AX, BX ; 将BX寄存器的值加到AX寄存器上 ADD AL, 1 ; 将立即数1加到AL寄存器上 ADD [1234h], AX ; 将AX寄存器的值加到内存地址1234h的值上
- 语法:
SUB - 减法指令
- 语法:
SUB destination, source
- 功能: 将源操作数的数据从目的操作数中减去,结果存储在目的操作数中。
- 示例:
SUB AX, BX ; 将BX寄存器的值从AX寄存器中减去 SUB AL, 1 ; 将立即数1从AL寄存器中减去 SUB [1234h], AX ; 将AX寄存器的值从内存地址1234h的值中减去
- 语法:
INC - 自增指令
- 语法:
INC destination
- 功能: 将目的操作数的值加1。
- 示例:
INC AX ; 将AX寄存器的值加1 INC [1234h] ; 将内存地址1234h的值加1
- 语法:
DEC - 自减指令
- 语法:
DEC destination
- 功能: 将目的操作数的值减1。
- 示例:
DEC AX ; 将AX寄存器的值减1 DEC [1234h] ; 将内存地址1234h的值减1
- 语法:
JMP - 无条件跳转指令
- 语法:
JMP label
- 功能: 无条件地跳转到指定的标签位置。
- 示例:
JMP START ; 跳转到标签START
- 语法:
CMP - 比较指令
- 语法:
CMP destination, source
- 功能: 比较目的操作数和源操作数,结果影响标志寄存器。
- 示例:
CMP AX, BX ; 比较AX和BX寄存器的值 CMP AL, 1 ; 比较AL寄存器的值和立即数1
- 语法:
JE/JZ - 条件跳转指令(等于/零)
- 语法:
JE label
或JZ label
- 功能: 如果比较结果为等于(零标志位被设置),则跳转到指定的标签位置。
- 示例:
CMP AX, BX JE EQUAL_LABEL ; 如果AX等于BX,则跳转到EQUAL_LABEL
- 语法:
JNE/JNZ - 条件跳转指令(不等于/非零)
- 语法:
JNE label
或JNZ label
- 功能: 如果比较结果为不等于(零标志位未被设置),则跳转到指定的标签位置。
- 示例:
CMP AX, BX JNE NOT_EQUAL_LABEL ; 如果AX不等于BX,则跳转到NOT_EQUAL_LABEL
- 语法:
示例程序
以下是一个简单的汇编程序示例,演示了上述指令的使用:
section .data
num1 dw 10
num2 dw 20
result dw 0
section .text
global _start
_start:
; 将num1的值加载到AX寄存器
MOV AX, [num1]
; 将num2的值加载到BX寄存器
MOV BX, [num2]
; 比较AX和BX
CMP AX, BX
; 如果AX等于BX,跳转到EQUAL_LABEL
JE EQUAL_LABEL
; 如果AX不等于BX,跳转到NOT_EQUAL_LABEL
JNE NOT_EQUAL_LABEL
EQUAL_LABEL:
; 如果相等,将AX的值加1
INC AX
JMP END
NOT_EQUAL_LABEL:
; 如果不相等,将AX的值减1
DEC AX
END:
; 将AX的值存储到result
MOV [result], AX
; 退出程序
MOV AX, 1
INT 0x80
第一章 打开电源以后发生的事
计算机历史
- 从白纸到图灵机
- 从图灵机到通用图灵机
- 从通用图灵机到计算机
打开电源
- 引导程序(扇区): 存储在0磁道0扇区(一个扇区一般是512字节),操作系统读入的第一段代码,操作系统的故事由此开始。。。
引导扇区代码bootset.s
,读入系统
.s
表示汇编代码- 汇编代码 和 高级语言代码的区别
汇编代码每一条都变成了机器指令,你能对其进行完整的控制,不想c语言中,int i;
你无法控制i到底最后会出现在内存的哪一个地址`
! Linux0.11的bootsect.s源代码
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text //文本段
begtext:
.data //数据段
begdata:
.bss //未初始化数据段
begbss:
.text
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
! ROOT_DEV: 0x000 - same type of floppy as boot.
! 0x301 - first partition on first drive etc
ROOT_DEV = 0x306
entry _start //entry关键字,告诉链接器程序入口
_start:
mov ax,#BOOTSEG
mov ds,ax ! 把0x07c0赋给ax,然后把ax付给ds(段寄存器)
mov ax,#INITSEG
mov es,ax ! es = ax = 0x9000
mov cx,#256
sub si,si ! si - si自己的值肯定等于0
sub di,di ! sub两行代码给出了两个地址值 分别是 ds:si = 0x7c00 和 es:di = 0x90000, 0x7c00就是一开始计算机启动时读入那个bootsect.s程序的起始地址,大小位512Byte!!!
rep movw ! 重复移动(w)字(cx的值256个字,512字节)
jmpi go,INITSEG ! jmpi是间接跳转; go(标号,将来编译完后,go的值就是从start开始的地方到go定义的偏移值)赋给ip寄存器,INITSEG(0x9000)赋给CS寄存器,其实相当于移完之后在跳转到改变后的go位置继续顺序执行代码
go: mov ax,cs
mov ds,ax
mov es,ax
DS(数据段寄存器):
- 作用:用来保存内存中存放数据的数据段地址。
- 默认偏移寄存器:在使用BX, SI和DI而省略段寄存器时,默认段寄存器是DS。但具体哪个偏移寄存器与DS结合使用,取决于具体的指令和上下文。不过,通常BX、SI和DI都可以与DS结合使用来寻址。
ES(附加段寄存器):
- 作用:存放当前执行程序中一个辅助数据段的段地址。这通常用于处理与DS不同的另一个数据段。
- 默认偏移寄存器:ES通常与DI(数据索引寄存器)结合使用,尤其是在涉及串操作的指令中。如参考文章2所述,默认的与段地址寄存器ES相结合的偏移地址寄存器是DI。
总结起来:
DS段寄存器的默认偏移寄存器可以是BX、SI或DI,具体取决于指令和上下文。
ES段寄存器的默认偏移寄存器是DI,特别是在串操作指令中。
int 0x10中断
! 打印加载语句的相关代码(同样来自bootsect.s)
! Print some inane message
mov ah,#0x03 ! read cursor pos ,BIOS中断功能号,读取光标位置
xor bh,bh !BH通常是页面号,设置为0
int 0x10 !调用BIOS中断
mov cx,#32 !使用之前计算的字符串长度
mov bx,#0x0007 ! page 0, attribute 7 (normal) 设置显示属性(正常文本)
mov bp,#msg1 !设置字符串的偏移地址
mov ax,#0x1301 ! write string, move cursor,BIOS中断功能号,写字符串并更新光标位置
int 0x10
! ok, we've written the message, now
...
msg1:
.byte 13,10
.ascii "Loading Xubin system ... "
.byte 13,10,13,10 !回车符ascii码值13,换行符10
这段汇编代码是一个简单的启动引导扇区实现,通常用于操作系统的引导程序。
entry _start
: 这行指定程序的入口点为_start
标签。_start:
: 定义了一个名为_start
的标签,标记着程序的起始位置。mov ah,#0x03
: 将常量0x03
移动到寄存器ah
中,通常用于设置视频模式。xor bh,bh
: 将寄存器bh
和自身执行异或运算,相当于将bh
寄存器清零。int 0x10
: 触发 BIOS 中断0x10
,用于调用视频服务例程。mov cx,#36
: 将常量36
移动到寄存器cx
中,通常用于设置字符串的长度。mov bx,#0x0007
: 将常量0x0007
移动到寄存器bx
中,通常用于设置文本输出的属性。mov bp,#msg1
: 将msg1
标签的地址移动到寄存器bp
中,用于指向消息字符串。mov ax,#0x07c0
: 将常量0x07c0
移动到寄存器ax
中。mov es,ax
: 将寄存器ax
中的值移动到段寄存器es
中,通常用于设置附加段。mov ax,#0x1301
: 将常量0x1301
移动到寄存器ax
中,用于在屏幕上显示字符串。int 0x10
: 再次触发 BIOS 中断0x10
,用于在屏幕上显示消息。inf_loop:
: 定义了一个名为inf_loop
的标签,用于创建一个无限循环。jmp inf_loop
: 无条件地跳转到inf_loop
标签处,实现一个无限循环。msg1:
: 定义了一个名为msg1
的标签,用于存储消息字符串。.byte 13,10
: 表示换行符。.ascii "Hello OS world, my name is LZJ"
: 存储要显示的消息字符串。.byte 13,10,13,10
: 包含连续的两个换行符,用于在屏幕上创建空行。.org 510
: 将当前汇编位置设置为内存地址510
,用于设置引导标志。boot_flag:
: 定义了一个名为boot_flag
的标签,用于存储引导标志。.word 0xAA55
: 将常量0xAA55
存储在boot_flag
标签处,用于指示 BIOS 这是一个引导扇区。
这段代码主要实现了在屏幕上显示消息字符串,并设置了引导标志,以便 BIOS 将其识别为引导扇区。最后,通过一个无限循环保持程序执行。
编译与运行
总结
- 实模式 与 保护模式
- 逻辑地址 与 物理地址
bootsect
就是开机时最先被读入内存(0x7c00
)的代码,(因为ip
和cs
的值决定取指执行从第0磁道第1个扇区
开始,存放的就是bootsect.s
,然后bootsect.s内部把自己转移到了0x90000
到0x90200
,并接着从0x90200
开始载入了setup
模块,打印开机提示”Loading System …”,最后把控制权交给了setup代码段(通过jmpi 0 0x9020
,CS:IP
= 0x9020:0,逻辑地址转换成物理地址就是0x90200
setup.s
操作系统接管硬件,获取计算机信息
初始化gdt(全局描述符表)
启动保护模式
保护模式下的地址翻译
jmp 0 8
指令的作用
makefile文件
操作系统最后编译出来叫做Image
镜像,
makefile是一种树状结构
system
模块
实验1
- 修改bootsect.s的代码,输出”Hello XubinOS, loading”
- bootsect.s 能完成 setup.s 的载入,并跳转到 setup.s 开始地址执行,在
setup.s 开始执行时需要向屏幕输出一行”Now we are in SETUP” 信息,表示我们进入了 setup 部分。ah,#0x03mov ah,#0x03 xor bh,bh int 0x10 mov cx,#25 mov bx,#0x0007 mov bp,#msg2 mov ax,cs mov es,ax mov ax,#0x1301 int 0x10 !死循环代码 inf_loop: jmp inf_loop msg2: .byte 13,10 .ascii "Now we are in SETUP!" .byte 13,10,13,10 .org 510 boot_flag: .word 0xAA55
操作系统接口
什么是接口?
系统调用是怎么实现的
应用程序如何系统调用?
在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。
调用自定义函数是通过 call 指令直接跳转到该函数的地址,继续运行。
而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:
把系统调用的编号存入 EAX;
把函数参数存入其它通用寄存器;
触发 0x80 号中断(int 0x80)。
我们不妨看看 lib/close.c,研究一下 close() 的 API:
#define __LIBRARY__
#include <unistd.h>
_syscall1(int, close, int, fd)
其中 _syscall1
是一个宏,在 include/unistd.h 中定义。
#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
将 _syscall1(int,close,int,fd) 进行宏展开,可以得到:
int close(int fd)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_close),"b" ((long)(fd)));
if (__res >= 0)
return (int) __res;
errno = -__res;
return -1;
}
这就是 API 的定义。它先将宏 __NR_close
存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res
,再通过对 __res
的判断决定传给 API 的调用者什么样的返回值。
其中 __NR_close 就是系统调用的编号,在 include/unistd.h 中定义:
#define __NR_close 6
/*
所以添加系统调用时需要修改include/unistd.h文件,
使其包含__NR_whoami和__NR_iam。
*/
/*
而在应用程序中,要有:
*/
/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__
/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"
/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);
/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);
在 0.11 环境下编译 C 程序,包含的头文件都在 /usr/include 目录下。
该目录下的 unistd.h 是标准头文件(它和 0.11 源码树中的 unistd.h 并不是同一个文件,虽然内容可能相同),没有 __NR_whoami
和 __NR_iam
两个宏,需要手工加上它们,也可以直接从修改过的 0.11 源码树中拷贝新的 unistd.h 过来。
从“int 0x80”进入内核函数
int 0x80
触发后,接下来就是内核的中断处理了。先了解一下 Linux0.11 处理 0x80
号中断的过程。
在内核初始化时,主函数(在 init/main.c 中,Linux 实验环境下是 main(),Windows 下因编译器兼容性问题被换名为 start())调用了 sched_init() 初始化函数:
void main(void)
{
// ……
time_init();
sched_init();
buffer_init(buffer_memory_end);
// ……
}
sched_init()
在 kernel/sched.c
中定义为:
void sched_init(void)
{
// ……
set_system_gate(0x80,&system_call);
}
set_system_gate
是个宏,在 include/asm/system.h
中定义为:
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
_set_gate 的定义是:
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
虽然看起来挺麻烦,但实际上很简单,就是填写 IDT(中断描述符表),将 system_call
函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call
。具体细节请参考《注释》的第 4 章。
接下来看 system_call。该函数纯汇编打造,定义在 kernel/system_call.s 中:
!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls = 72
!……
.globl system_call
.align 2
system_call:
! # 检查系统调用编号是否在合法范围内
cmpl \$nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
! # push %ebx,%ecx,%edx,是传递给系统调用的参数
pushl %ebx
! # 让ds, es指向GDT,内核地址空间
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx
! # 让fs指向LDT,用户地址空间
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax)
jne reschedule
cmpl $0,counter(%eax)
je reschedule
system_call 用 .globl 修饰为其他函数可见。
Windows 实验环境下会看到它有一个下划线前缀,这是不同版本编译器的特质决定的,没有实质区别。
call sys_call_table(,%eax,4)
之前是一些压栈保护,修改段选择子为内核段,call sys_call_table(,%eax,4)
之后是看看是否需要重新调度,这些都与本实验没有直接关系,此处只关心 call sys_call_table(,%eax,4)
这一句。
根据汇编寻址方法它实际上是:call sys_call_table + 4 * %eax,其中 eax 中放的是系统调用号,即 __NR_xxxxxx
。
显然,sys_call_table 一定是一个函数指针数组的起始地址,它定义在 include/linux/sys.h 中:
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...}
增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iam
和 sys_whoami
。当然该函数在 sys_call_table 数组中的位置必须和 __NR_xxxxxx 的值对应上。
同时还要仿照此文件中前面各个系统调用的写法,加上:
extern int sys_whoami();
extern int sys_iam();
不然,编译会出错的。
实现 sys_iam() 和 sys_whoami()
添加系统调用的最后一步,是在内核中实现函数 sys_iam()
和 sys_whoami()
。
每个系统调用都有一个 sys_xxxxxx() 与之对应,它们都是我们学习和模仿的好对象。
比如在 fs/open.c 中的 sys_close(int fd):
int sys_close(unsigned int fd)
{
// ……
return (0);
}
它没有什么特别的,都是实实在在地做 close() 该做的事情。
所以只要自己创建一个文件:kernel/who.c
,然后实现两个函数就万事大吉了。
修改makefile文件
要想让我们添加的kernel/who.c
可以和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。
Makefile 里记录的是所有源程序文件的编译、链接规则,《注释》3.6 节有简略介绍。我们之所以简单地运行 make 就可以编译整个代码树,是因为 make 完全按照 Makefile 里的指示工作。
如果想要深入学习 Makefile,可以选择实验楼的课程: 《Makefile 基础教程》、《跟我一起来玩转 Makefile》。
Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是 kernel/Makefile。需要修改两处。
(1)第一处
OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o
改为:
OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o who.o
添加了 who.o。
(2)第二处
### Dependencies:
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
../include/asm/segment.h
改为:
### Dependencies:
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
../include/asm/segment.h
添加了 who.s
printf() who.o: who.c ../include/linux/kernel.h ../include/unistd.h
。
Makefile 修改后,和往常一样 make all
就能自动把 who.c 加入到内核中了。
如果编译时提示 who.c
有错误,就说明修改生效了。所以,有意或无意地制造一两个错误也不完全是坏事,至少能证明 Makefile 是对的。
用printk()
调试内核
oslab 实验环境提供了基于 C 语言和汇编语言的两种调试手段。除此之外,适当地向屏幕输出一些程序运行状态的信息,也是一种很高效、便捷的调试方法,有时甚至是唯一的方法,被称为“printf 法”。
要知道到,printf()
是一个只能在用户模式下执行的函数,而系统调用是在内核模式中运行,所以 printf()
不可用,要用 printk()
。
printk()
和 printf()
的接口和功能基本相同,只是代码上有一点点不同。printk()
需要特别处理一下 fs
寄存器,它是专用于用户模式的段寄存器。
看一看 printk 的代码(在 kernel/printk.c 中)就知道了:
int printk(const char *fmt, ...)
{
// ……
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
"pushl $buf\n\t"
"pushl $0\n\t"
"call tty_write\n\t"
"addl $8,%%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (i):"ax","cx","dx");
// ……
}
显然,printk()
首先 push %fs
保存这个指向用户段的寄存器,在最后 pop %fs
将其恢复,printk()
的核心仍然是调用 tty_write()
。查看 printf()
可以看到,它最终也要落实到这个函数上。
编写测试程序
激动地运行一下由你亲手修改过的 “Linux 0.11 pro++”!然后编写一个简单的应用程序进行测试。
比如在 sys_iam() 中向终端 printk() 一些信息,让应用程序调用 iam(),从结果可以看出系统调用是否被真的调用到了。
可以直接在 Linux 0.11 环境下用 vi 编写(别忘了经常执行“sync”以确保内存缓冲区的数据写入磁盘),也可以在 Ubuntu 或 Windows 下编完后再传到 Linux 0.11 下。无论如何,最终都必须在 Linux 0.11 下编译。编译命令是:
$ gcc -o iam iam.c -Wall
gcc 的 “-Wall” 参数是给出所有的编译警告信息,“-o” 参数指定生成的执行文件名是 iam,用下面命令运行它:
$ ./iam
如果如愿输出了你的信息,就说明你添加的系统调用生效了。否则,就还要继续调试,祝你好运!
实验
题目:
此次实践项目的基本内容是添加两个系统调用,并编写简单的应用程序进
行测试。
第一个系统调用是iam
,其函数原型为:int iam(const char* name);
完成的功能是将字符串参数name的内容拷贝到内核中并保存下来。要求name的长度不能超过23个字符,返回值是拷贝的字符数。
如果name的字符个数超过了23,则返回“-1”,并置errno为EINVAL。
第二个系统调用是whoami
,其函数原型为:int whoami(char*name,unsigned int size);
该系统调用将内核中由iam()
保存的名字拷贝到name指向的用户地址
空间中,同时确保不会对name越界访存(name的大小由size说明)。
返回值是拷贝的字符数。如果size小于需要的空间,则返回“-1”,并置errno为EINVAL
。
测试程序是编写两个用户态测试程序iam.c和whoami.c,其中iam.c要通过
系统调用iam设置内核中的一个字符串,而whoami.c则通过系统调用是whoami
来取出这个内核字符串,并用printf在屏幕上输出。最终的运行结果是:
[usr/root]# ./iam
lizhijun
[usr/root]# ./whoami
lizhijun
- 具体步骤
- 先在oslab目录下->sudo ./mount-hdc,挂载Bochs虚拟机
- 在~/oslab/hdc/usr/root下书写 iam.c,whoami.c
- 修改
linux0.11/kernel/syscall.s
中的系统调用总数为74(原本是72) linux-0.11/include/linux/sys.h
修改调用表,添加上你要增加的系统调用
…..
blog详细教程