完成setup后, *** 作系统的代码都被读入到从0地址开始的地方,还创建了一些初始的结构,如mem_map(管理内存的数据结构)、GDT、IDT等。而我们的应用程序都放在了内存的上端。
最终,内存的下方放置的为系统代码和数据、上方放置的为应用程序,这样子一个结构情况。
(1)什么是 *** 作系统接口?
系统调用(接口表现为函数调用,又由系统提供)
(2) *** 作系统接口连接谁?
连接 *** 作系统和应用软件
(3)如何连接?
C语言程序
其中,printf
是包装了write
(系统调用函数)之后的函数。
POSIX是统一的接口
2、系统调用的实现 (0)函数处理过程(1)假设用户程序内使用printf()
函数;
(2)根据lib
下的_syscalln()
和include/unistd.h
下的模板,对printf()
函数进行宏定义展开;
(3)调用展开后的函数,触发80中断,将kernel
下的system_call
对应的IDT表中的DPL设为3,从而让用户程序可获取system_call
地址作为IP。然后,再设置CS=8,使其对应的CPL=0,从而让用户可以进入内核态;
(4)在system_call
函数中,会使用从include/unistd.h
中获得的存入eax
的值,来查询include/linux/sys.h
中sys_call_table
表里对应的系统调用函数;
(5)使用对应的系统调用函数处理数据后,将结果存入eax
并返回给用户程序。
*** 作系统实现系统调用的基本过程
(1)应用程序调用库函数(API);
(2)API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
(3)内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
(4)系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
(5)中断处理函数返回到 API 中;
(6)API 将 EAX 返回给应用程序。
整个过程中主要通过EAX传递数值。
在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。
-
调用自定义函数是通过
call
指令直接跳转到该函数的地址,继续运行。 -
调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:
(1)把系统调用的编号存入 EAX;
(2)把函数参数存入其它通用寄存器;
(3)触发 0x80 号中断(int 0x80)。
linux-0.11 的 lib
目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell
。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。
通过硬件实现内核的数据不能被随意的调用,不能随意的jmp。否则, *** 作系统就会不安全。
系统调用实际上提供了一种进入内核的手段。
将内存分割成内核段
和用户段
两个区域。内核态可以访问任何数据,但用户态不能访问内核数据。只有当前的指令大于或等于目标的特权级,这条指令才被允许执行。
不论是内核段
还是用户段
都需要通过段寄存器进行访问,主要使用了两个段寄存器CPL
和DPL
来实现不同权限的控制。其中CPL
存放在CS中,DPL
存放在GDT
中。当想访问其他段时,会从GDT中查询目标段的DPL来和当前所执行段CS中的CPL进行对比。若合法则允许访问,若不合法则不允许访问。
保护模式中最重要的一个思想就是通过分级把代码隔离了起来
,不同的代码在不同的级别 ,使大多数情况下都只和同级代码发生关系。 在保护模式下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。
Intel 的80286以上的CPU可以识別4个特权级(或特权层) ,0级到3级
。数值越大特权越小
。一般用把系统内核放在0级,系统的其他服务程序位于1、2级,3级则是应用软件。一般情况下代码都在自己的级别下做自己 的工作,同一级别之间可以相互访问,而一般是不允许不同级别的代码间随意访问的。但有时候不同级别的程序之间一定要访问,比如系统的接口函数等,必须能够使得应用程序能够随意调用。0
表示内核态,3
表示用户态。
DPL(Descriptor Privilege Level):描述符特权
用于描述目标内存段(要跳转访问的目标段)的特权级。 存储在描述符中的权限位,用于描述代码的所属的特权等级,也就是代码本身真正的特权级。一个程序可以使用多个段(Data,Code,Stack)也可以只用一个code段等。正常的情况下,当程序的环境建立好后, *** 作系统已经初始化好了DPL,段描述符都不需要改变——当然DPL也不需要改变,因此每个段的DPL值是固定。DPL在GDT中,一个GDT表的表项用于描述一段内存。OS中区域无论是数据段还是代码段,GDT表中对应的DPL均为0。
CPL(Current Privilege Level):当前任务特权
用于描述当前的执行内存段的特权级。 它的特权级是3
,表示用户态
。
中断是进入内核的唯一方法,该方法通过硬件来实现。因此,如果用户程序
想要进入内核,就需要包含一段int指令
的代码,这段代码由库函数实现,由宏来展开成一段汇编代码。进入内核之后, *** 作系统就会写中断处理过程,来获取想调程序的编号
。然后, *** 作系统会根据编号执行相应的代码。
规定只有int 0x80
中断才能进入内核态。
在include/linux/sys.h
中
#define __NR_write 4
在lib/close.c
中
#define __LIBRARY__
#include
_syscall1(int, close, int, fd)
在include/unistd.h
中
#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int \x80" :
"=a" ( )__res\ :
"0" ( ##__NR_)name,"b"( (long)()a));\ if
( 0__res >= )\ return
( )type; __res\ =
errno - ;__res\ return
- 1;\ }
_syscall1(int,close,int,fd)
将int 进行宏展开,可以得到:
close (int) fdlong
{
; __resvolatile
__asm__ ( "int :x80""=a"
( ) :__res"0"
( ) ,__NR_close"b"(( long)())fd);if(
0 )__res >= return(
int );= __res-
errno ; return__res-
1 ;}#define _syscall1(定义的函数类型, 定义的函数名, 入口参数类型, 入口参数名)
%0
格式:
(__NR_##name)
#define 来定义宏。该命令允许把一个名称指定成任何所需的文本。 在定义了宏之后,无论宏名称出现在源代码的何处,预处理器都会把它用定义时指定的文本替换掉。
#与 ## 是俩个特殊符号,# 表示将一个宏参数变成一个字符串,## 表把俩个字符串粘在一起。惯例将宏名称每个字母采用大写,这有助于区分宏与一般的变量。
0或空表示使用与相应输出一样的寄存器。
a表示使用eax,并编号%0。
拓展资料:
宏函数
C语言宏的定义和宏的使用方法(#define)
内嵌汇编学习
C语言的内嵌汇编
嵌入汇编程序规定把输出和输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以 “%0” 开始,分别记为 %0 、 %1 、 …%9 。因此,输出寄存器的编号是 %1
(这里只有一个输出寄存器),输入寄存器前一部分 ((long)(a)))
的编号是1%
,而后部分_syscall3
的编号__NR_write
。
用宏定义eax
来调用展开。在实现过程中,先将宏fd
(一个系统调用号)存入ebx
,将参数*buf
存入ecx
,count
存入edx
,int 0x80
存入eax
。
输入完参数后,就执行__res
指令,在内核态根据获取到的参数,去执行相应的系统调用函数。执行完内核态程序后,再把__res
中的值置给return (type)__res
。最后根据return -1
的值,决定执行int write()
或int 0x80
,返回int 0x80
的返回值。
include/unistd.h
eax
触发后,接下来就是内核的中断处理了。上述的__res
的执行过程,实际上就是从IDT表里面取出中断处理函数,然后跳到对应位置去执行。中断处理函数执行完后,再回来把init/main.c
赋给main()
。
首先,了解一下 0.11 处理 0x80 号中断的过程。
在内核初始化时,主函数(在 start()
中,Linux 实验环境下是 sched_init()
,Windows 下因编译器兼容性问题被换名为 void)调用了 main 初始化函数:
( void)// ……time_init
{
(
);sched_init(
);buffer_init(
);buffer_memory_end// ……}
sched.c
sched_init()
kernel/sched.c
是内核中有关任务(进程)调度管理的程序,其中包括有关调度的基本函数(sleep_on()、wakeup()、schedule()等)以及一些简单的系统调用函数(比如getpid())。系统时钟中断处理过程中调用的定时函数do_timer()也被放置在本程序中。
void在sched_init中定义为:
( void)// ……set_system_gate
{
(
0x80,&);system_call}0x80
n
将system_call
传递给了addr
,set_system_gate
函数的地址传递给了include/asm/system.h
。
#是个宏,在define中定义为:
set_system_gate( ,)n\addr_set_gate (
&[]idt,n15,3,)n
addraddr
其中set_system_gate
表示中断号,_set_gate
表示地址。
然后,&idt[n]
又调用了n
这个宏。gate_addr
(idt是一个全局标量,它是IDT表的起始地址,用addr
来找到80号中断对应的表项)会传递给15
参数。3
就表示上述的地址,type
和dpl
分别传到了|和=。
这段代码主要是初始化IDT表,然后再根据中段指令去查表,跳转到对应地址进行执行。
对应IDT结构
= 处理函数入口点偏移&addr||system_call 3 p | 01110 | | | =
对应CS:IP结构
0x0008 段选择符|= = 处理函数入口点偏移&addr|_set_gate
system_call 设置门描述符宏
此时DPL=3,而CS=8,IP=&system_call,其中CS的最后两位为0,即CPL=0。
// 根据参数的中段或异常处理过程地址addr、门描述符类型type和特权级信息dpl,设置位于地址 gate_addr 处的门描述符。的定义是:
// 注意:下面“偏移”值是相对于内核代码或数据段来说的。
// %0 — 由dpl,type组合成的类型标志字;%1 — 描述符低4字节地址
// %2 — 描述符高4字节地址; %3 — edx(程序偏移地址addr); %4 — eax(高字中含有段选择符)
#
define
_set_gate( ,,gate_addr,type)dpl\addr__asm__ (
"movw %%dx,%%ax\n\t" \ // 将偏移地址低字与选择符组合成描述符低4字节(eax)。 "movw %0,%%dx\n\t"// 将类型标志字与偏移高字组合成描述符高4字节(edx)。
"movl %%eax,%1\n\t" \ // 分别设置门描述符的低4字节和高4字节。
"movl %%edx,%2" \ :
: \
"i" \
( ( short)(0x8000 +(<<13dpl)+(<<8type))),// 1111<<13 — 1 1110 0000 0000 0000、0011<<8 — 0011 0000 0000"o" \ (
* ((char*) () )gate_addr),// gate_addr — 0x80"o" \ (
* (4+(char*) () )gate_addr),"d"( \
( char*) () )addr,"a"(0x00080000 ))// addr — &system_callgate_addr
type
参数:
dpl
描述符地址:指定了描述符所处的物理内存地址;
addr
描述符类型域值:指明所需设置的描述符类型,type=14(0x0E)表示中断门描述符,type=15(0x0F)表示陷阱门描述符;
注描述符特权级:对应描述符格式中的DPL(Descriptor Privilege Level);
system_call
偏移地址:是描述符对应的中断处理过程的32位偏移地址
0x80
:因为中断处理过程属于内核段代码,所以它们的段选择符值均为0x0008(在eax寄存器高字中指定)。
虽然代码看起来挺麻烦,但实际上很简单,就是填写 IDT(中断描述符表),将 0x80
函数地址写到 system_call
对应的中断描述符中,也就是在中断 int 0x80
发生后,自动调用函数 eax
。
详细过程
__res
需要根据IDT表找到中断处理函数,然后调到那里去执行,处理完之后再回来,再去执行把int 0x80
赋值给system_call
*** 作。
在初始化的时候,system_call
需要通过movl %%eax, %1
来进行处理。该处理通过中断处理门来实现,核心是初始化好IDT。一旦初始化完毕后,后续再遇到80中断时,就直接从IDT中取出相应的中断处理函数(eax
),然后调到对应地方去执行。
%1
是将"o"(*((char*)(gate_addr)))
赋给了addr
(&system_call
)。
最后实现将dpl
=0x0008
组装到了处理函数入口点偏移,把system.h
=3组装到了DPL,将move_to_user_mode()
组装到了段选择符。所以现在,CS=8,IP=&system_call。
因为当CS=8时,CS的最后两位CPL就等于00。
总结
在初始化的时候将80号中断的DPL设为3,故意让用户态程序能够进来。进来之后,CPL就会根据CS=8,其中CPL=0,进入到内核态。执行完内核态中代码后,CS的最后两位又会被设置为3,又变成了用户态的东西。
拓展资料:
什么是调用门?
中断描述符(IDT)、任务门、中断门、陷阱门
kernel/system_call.s
中定义了设置或修改描述符/中断门等的嵌入式汇编宏。其中,函数!是用于内核在初始化结束时人工切换(移动)到初始进程(任务0)去执行,即从特权级0代码转移到特权级3的代码中去运行。
使用这种方法进行控制权的转移是由CPU保护机制造成的。CPU允许低级别(例如特权级3)的代码通过调用门或中断、陷阱门来调用或转移到高级别的代码中运行,但反之则不允许。因此内核采用了这种模式IRET返回低级别代码的方法。
在!中
=……
72 # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls ! .
.……
2globl system_call
:align !
system_call-
1 # 检查系统调用编号是否在合法范围内
cmpl \$nr_system_calls,%-1eax # 调用好如果超出范围就在eax中置%%并对出
ja bad_sys_call
push %ds # 保存原段寄存器值
push 3es
push %fs
# 一个系统调用最多可带3个参数,也可不带参数
pushl %edx # 存放第2个参数
pushl !ecx # 存放第%个参数
, # push %ebx,%ecx%1edx,是传递给系统调用的参数
pushl !ebx # 存放第,个参数
0x10 # 让ds, es指向GDT,内核地址空间
movl $%%,edx
mov %dx%,ds
mov %dx,0x17es # ds,es指向内核数据段(全局描述符表中数据段描述符)。
movl $%!%edx
, # 让fs指向LDT(局部数据段,局部描述符表中数据段描述符),用户地址空间。指向执行本次系统调用的用户程序的数据段。
mov %dxsys_call_table(fs
call ,%,4eax)=[ # 间接调用指定功能C函数。调用地址+%sys_call_table * 4eax ] [],其中sys_call_table//是一个指针数组,在include.linux%sys0h中
pushl =eax # 把系统调用返回值入栈
# 查看当前任务的运行状态。如果不在就绪状态(state≠0),则去执行调度程序。
# 如果该任务在就绪状态,但时间片已用完(counter,%),则也去执行调度程序。
movl current0,eax # 取当前任务(进程)数据结构地址-> eax。
cmpl $state(%)0eax, # state
jne reschedule
cmpl $counter(%)system_call
eax.globl
# counter
je reschedule
call sys_call_table(,%eax,4)
用 注: 修饰为其他函数可见。Windows 实验环境下会看到它有一个下划线前缀,这是不同版本编译器的特质决定的,没有实质区别。
call sys_call_table(,%eax,4) 之前的代码主要实现一些压栈保护,修改段选择子为内核段。
call sys_call_table(,%eax,4) 之后的代码是看看是否需要重新调度,这些都与本实验没有直接关系,此处只关心 sys_call_table
这一句。
include/linux/sys.h
每个函数都是4个字节(32位),所以乘上4。
显然,fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, sys_write, ...
一定是一个函数指针数组的起始地址,它定义在 call _sys_call_table(,%eax,4)
中:
call sys_call_table + 4 * %eax
__NR_xxxxxx
汇编寻址方法它实际上是:sys_call_table
,其中 eax 中放的是系统调用号(可看作该函数的数组下标),即 4
。sys_write
就是基址(是一个函数表)。edx
表示每个系统调用对应的函数占四个字节(32位)。当要查找dx
(位于第5个函数)时,会设置 eax=4(数组下标从0开始)。
将es
置为10,然后将10
和10
都置为10。而都置为iret
的原因是,printf
的最后两位也是0。
其中,printf
对应的处理函数就放在第四个位置上。
(1)用户调用_syscall3
函数(库函数)。
(2)然后unistd.h
会展开宏定义eax
,查找int 0x80
中对应的系统调用号,将其传入set_system_gate
中,等其余寄存器存储好其余参数后,触发set_system_gate
中断,调用system_call
函数。此时为用户态,调用DPL
后,会将CPL
在IDT中的system_call
设为3,从而让用户程序可以使用其对应地址作为IP。之后,再将CS:IP中的eax
设为0,让用户程序可进入内核态。
(3)进入内核态中,调用system_call_table
来根据unisted.h
查找表__NR_write
中对应的系统调用函数。
(4)根据已在call sys_call_table(,%eax,4)
获得的宏定义call sys_call_table + 4 * %eax
=4,再调用call sys_write
指令,即eax
也就是_syscall3
,来调用目标函数进行执行。
(5)执行完后,将结果存入CPL
中,返回到DPL
中宏定义的函数,作为其返回值。
用户态中int 0x80
=3,内核态中DPL
=0。用户程序不能随意的进入内核中调用,要想进去用户必须要先设置系统调用号,通过中断CPL
,才能通过接口调用内核程序。想要“穿过”内核的方式是将CPL
也设置为和_system_call
相同的数,一旦“穿过”去之后,sys_whoami
就被置为0。在sys_whoami()
里面通过移动查表就会调用linux-0.11/include/unisted.h
,然后就会跑到内核中真正的__NR__xxxxx
函数调用。
在注中添加系统调用编号,格式/usr/include
。
system_call.s
: 在 0.11 环境下编译 C 程序,包含的头文件都在 nr_system_calls
目录下。如果只在这里修改会报错,后面会说明。
在include/linux/sys.h
中增加了两个系统调用,所以将系统调用总数fn_ptr sys_call_table[]
更改为74。
这是系统调用总数。如果增删了系统调用,必须做相应修改。
在sys_whoami
中,找到sys_iam
,在里面添加extern int sys_whami()
和extern int sys_iam()
这两个函数引用。同时,在上面也添加注和sys_call_table
。
unisted.h
: 函数在 __NR_xxxxxx
数组中的位置必须和 kernel
中的who.c
的值对应上。
在#中创建include并修改
#include
#include
#include
static char
[ 24 str];staticunsigned
long ; int lensys_iam
( constchar* )int name, {
; ichar j[
24 tmp];for(
=0i ; <24 i ; ++) i[] {
tmp=iget_fs_byte ( +)name ; iif(
[]tmp==i0 ) break; }=
+
len 1 i ; // clear strfor
(
=0j ; <24 j ; ++) j[] {
str=j0 ; }if
(
==24i ) printk( {
"Length over 23! Please enter again!\n");return-
( );EINVAL}// copy
for
(
=0i ; [] tmp!=i0 ; ++) i[] {
str=i[ ] tmp;i}return
;
} lenint
sys_whoami
( char*,unsigned nameint ) int size; {
if i(
<)size return len- {
( );EINVAL}for
(
=0i ; <&& i [ size ] str!=i0 ; ++) iput_fs_byte( {
[]str,i+) name ; i}put_fs_byte
(
0,+) name ; ireturn;
} len注:
int i
make all
不能在for循环里定义-1
否则errno
时会报错。
实验内容要求系统调用API在参数不合理时返回EINVAL
并置errno
为return -(EINVAL)
。从下面的宏展开可知,#是一个存在于用户空间的全局变量,其值是系统调用处理程序返回值的负值,所以系统调用服务例程在参数不合理时应写成define。当传进来的字符串过长,需要return -(EINVAL)。
_syscall1( ,,type,name)atype\aname (
type )\atype a\ long
{ ;
\ __resvolatile (
__asm__ "int \x80" :"=a" (
) \ :__res"0" (
## ) ,__NR_"b"name((long )()))a;\if( 0
) \__res >= return( )
; \type= __res- ;
errno \ return__res- 1
; \}# define
_syscall2
(, ,,type,name,atype)a\btypenameb( ,
type )\atype a\btype blong ;
{ \
volatile __res( "int \x80"
__asm__ : "=a"( )
\ : "0"__res( ##
) , "b"__NR_(name(long) ()),"c"a((long) ()));b\if(0 )
\ return__res >= () ;
\ =type- __res; \
errno return -__res1 ;
\ }kernel/Makefile
OBJS
Dependencies
OBJS
(4)修改Makefile
Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是who.o
中的# Despendencies
和who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
中的内容:
linux-0.11
处加上make all
在who.c
下添加iam
然后在whoami
下使用/usr/include/unistd.h
就能自动把 _syscalln()
加入到内核中了。
想使用系统调用#和define ,就需要有相应的系统调用API。在 _syscall1 中,有linus预先写好的系统调用API宏模板 ( ,其中n表示的是系统调用的参数个数。
,, ,)type\namenameatype(a) \
type \longatype a; \
{ volatile
( __res"int \x80" :
__asm__ "=a" () \
: "0" (__res## )
, "b" (__NR_(namelong)( )));\aif(0) \
return (__res >= ); \
= -type; __res\ return
errno - 1__res; \
} #define_syscall2 (
,
,, ,,type)name\atypenamea(btype,b) \
type \longatype a;btype b\ volatile
{ (
"int \x80" __res: "=a"
__asm__ ( )\ :
"0" ( ##__res) ,
"b" ( (__NR_longname)() ),"c"((along)() ));\ifb(0)\ return
( )__res >= ;\ =
- ;type\ __resreturn -
errno 1 ;__res\ }
iam.c
whoami.c
/* 有它,_syscall1 等才有效。详见unistd.h */# define
__LIBRARY__
在oslab目录下编写/* 有它,编译器才能获知自定义的系统调用的编号 */,#
iam.c
include
"unistd.h"# include
_syscall1
(int ,
,char
*,); iamint main(int name,char
* []) argcif (<= argv1)printf {
("input error\n"argc ) ;return{
-1;}iam
( [1]
)
;returnargv0;}#define
__LIBRARY__ #include
"unistd.h"
whoami.c
#include _syscall2
(int ,
,char
*,,unsigned whoamiint ,); namechar [ 24] size=}
; nameintmain( int {,char
* []) argcwhoami (, argv24); {
printf(name"%s\n" ,);
return0;} name注//
. /-
/usr/root/
oslab/
: 不能用.来注释,否则在linux0.11中会报错。
然后,将这两个文件以挂载的方式实现宿主机与虚拟机 *** 作系统的文件共享,在 oslab 目录下执行以下命令挂载hdc目录到虚拟机 *** 作系统上。
sudo ./mount/hdc
再通过以下命令将上述两个文件拷贝到虚拟机linux-0.11 *** 作系统卸载
sudo umount hdc
目录下,命令在-目录下执行:
cp iam.c whoami-c hdc.usrunistd.h
root
查看是否挂载成功
读写完毕后再卸载
linux-0.11
(7)编译iam.c和whoami.c
可以直接在 Linux 0.11 环境下用 vi 编写(别忘了经常执行“sync”以确保内存缓冲区的数据写入磁盘),也可以在 Ubuntu 或 Windows 下编完后再传到 Linux 0.11 下。无论如何,最终都必须在 Linux 0.11 下编译。编译命令是:
gcc hdc/usr/include
o iam iamunistd.h
c
gcc o whoami whoamic
gcc 的 “-Wall” 参数是给出所有的编译警告信息,“-o” 参数指定生成的执行文件名是 iam。
出现报错,原因是:
之前修改的没有加载到中,需要打开挂载后,进入中去修改。
再用gcc编译,若无提示信息, 则编译成功
成功!
参考资料:
*** 作系统实验(二)——系统调用
超详细! *** 作系统实验三 系统调用(哈工大李治军)
哈工大- *** 作系统实验-李治军-实验2:系统调用
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)