这次实验的基本内容是:在Linux0.11上添加两个系统调用linux系统调用,并编撰两个简单的应用程序测试它们。
具体实验细节可以参考蓝桥云:
后置内容1.哪些是系统调用
系统调用是操作系统实现硬件前馈与封装,为下层软件提供插口调用的一种途径。应用程序通过系统调用恳求操作系统的服务。系统中的各类共享资源都由操作系统统一执掌,因而在用户程序中,但凡与资源有关的操作(如储存分配、I/O操作、文件管理等),都必须通过系统调用的方法向操作系统提出服务恳求,由操作系统代为完成。简单来说就是操作系统提供统一的封装函数,用户想要实现对底层硬件资源的使用,就只能通过操作系统提供的API来完成。这样才能确保程序的相对安全与稳定。
所以系统调用便提供了从用户模式才能访问内核模式的途径。在Linux中实现系统调用是借助了软件中断的模式来实现,将系统调用设置成了一种特殊的中断模式。
其中int$0x80便是实现的惟一的汇编指令,其余代码均是指定输入输出以及指令执行过程中的寄存器及变量的使用。
int$0x80指令属于软中断(softwareinterrupt)。软中断又称作编程异常(programmedexception),是异常的一种。该指令的作用是以0x80作为索引值,用于在中断描述符表IDT中查找储存了中断处理程序信息的描述符。
之前我们也剖析过IDT中断描述符表的结构,中断描述符表(InterruptDescriptorTable,IDT)是拿来告诉处理器在碰到异常或“INT”操作码(汇编中)时所应调用的中断服务解释器(InterruptServiceRoutine,ISR)。简单来说就是会有一段内核空间专门拿来储存中断程序的地址,索引值以及优先级。其中set_system_gate(0x80,&system_call)便是拿来设置系统调用函数位置的函数。这儿具体的设置细节内容就不细讲了。
当设置好了系统调用中断,当触发0x80中断,操作系统便会依照idt中记录的入口函数地址因而调用system_call函数
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 as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
ret_from_sys_call:
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f
system_call会模拟函数调用过程。可以看见system_call函数首先将DS,ES,FS这种数据段寄存器压栈,之后将保存在EBX,ECX,EDX的库函数API的参数倒序压栈。接着通过callsys_call_table(,%eax,4)调用sys_call_table(%eax储存的系统调用号)因而运行sys_call_table中对应的各类系统调用函数。当系统调用函数结束模拟正常的函数返回过程,因而实现内核态到用户态的切换。
大致过程如右图:
因而本次的实验两个系统调用函数只须要在sys_call_table中存入相应的系统调用号和函数入口地址,之后编撰sys_iam(),sys_whoami()两个函数即可。系统调用和内核态到用户态的切换过程sys_call()函数都早已帮我们封装好了。
2.通过sys_fork()函数举例
当我们运行一个内核态的fork()函数,fork通过0x80中断(eax=2)进行系统调用,通过system_call()运行了syscall_table中系统调用号为2的sys_fork()系统调用函数,因而实现了fork过程。
fork()函数
void main(void) {
...
move_to_user_mode();
if (!fork()) {
init();
}
for(;;) pause();
}
move_to_user_mode早已让我们目前正在运行的程序模式转到用户态,处于受限状态,假如须要进行特殊操作,须要通过中断深陷内核态才可以。因而此时的fork()早已运行在了用户态,属于进程代码,他的代码段属于进程代码段
会依照局部LDT和TSS储存在对应的进行虚拟显存中。而内核态的代码与资源储存在全局描述符表的数据段与代码段中。因而此时的fork()函数须要在函数内部调用系统调用函数,步入内核态,因而才能实现进程空间中代码段数据段与全局描述符表中的代码段与数据段空间进行交互访问与运算。
fork()函数
static _inline _syscall0(int,fork)
#define _syscall0(type,name)
type name(void)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_##name));
if (__res >= 0)
return (type) __res;
errno = -__res;
return -1;
}
所以linux系统调用,把宏定义都展开,虽然就相当于定义了一个函数。
int fork(void) {
volatile long __res;
_asm {
_asm mov eax,__NR_fork
_asm int 80h
_asm mov __res,eax
}
if (__res >= 0)
return (void) __res;
errno = -__res;
return -1;
}
关键指令就是一个0x80号软件中断的触发,int80h。其中还有一个eax寄存器里的参数是__NR_fork,这也是个宏定义,值是2。按照上文的介绍,当执行了int80h时,便会触发system_call()函数并在模拟正常的函数调用,并在system_call()中调用system_call_table[eax对应的系统调用号(这儿是__NR_fork)]跳转到sys_fork()的函数入口。
sys_call_table()
那我们接着看sys_call_table是个啥。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
这儿的fn_ptr类型是int(*)(),表示一个返回类型为int的函数表针,简单说就是fn_ptr储存的就是对应函数的入口地址。
extern int sys_setup();
extern int sys_exit();
extern int sys_fork();
extern int sys_read();
extern int sys_write();
extern int sys_open();
extern int sys_close();
......
extern int sys_setsid();
extern int sys_sigaction();
extern int sys_sgetmask();
extern int sys_ssetmask();
extern int sys_setreuid();
extern int sys_setregid();
在include/linux/sys.h中早已定义了对应的函数。因而加入我们要设计一个对应的系统调用函数,我们只须要在sys.h中添加一个extern(*int)()函数,并将函数入口地址添加到table中,接出来我们就只须要编撰sys_call()的函数功能即可。诸如sys_fork()函数
sys_fork()
_sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
这个函数功能就不具体讲了。从这儿的探求我们也可以看出,操作系统通过系统调用,提供给用户态可用的功能,都曝露在sys_call_table里了。系统调用统一通过int0x80中断来步入,具体调用这个表里的那个功能函数,就由eax寄存器传过来,这儿的值是个字段索引的下标,通过这个下标就可以找到在sys_call_table这个字段里的具体函数。
同时也可以看出,用户进程调用内核的功能,可以直接通过写一句int0x80汇编指令linux服务器搭建,而且给eax形参,其实这样就比较麻烦。
所以也可以直接调用fork这样的包装好的方式,而这个方式里本质也是int0x80以及eax形参而已。
这儿再放一下刚刚的图:
3.GCC内联汇编
可以参考如下文章:
内联汇编&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-1-121372941.142v87insert_down28,239v2insert_chatgpt&spm=1018.2226.3001.4187
大致格式如下:
asm volatile
( 这里写指令
: 输出操作数 /* 可选 */
: 输入操作数 /* 可选 */
: 可能被破坏的寄存器列表 /* 可选 */
);
或者
__asm__ __volatile__
( 这里写指令
: 输出操作数 /* 可选 */
: 输入操作数 /* 可选 */
: 可能被破坏的寄存器列表 /* 可选 */
);
输入输出操作数格式:
[ [asmSymbolicName] ] constraint (cexpression)
例如:
[a_val]"r"(a), [b_val]"r"(b)
"r"(a), "r"(b)
插入到C代码中的汇编句子是以:分隔的四个部份,第一部份是汇编代码本身,一般成为指令部。指令部是必须的linux 删除文件夹,而其它部份可以依据实际情况而省略。GCC采用如下方式来解决汇编代码中操作数怎样与C代码中的变量相结合的问题:对寄存器的使用只需给出**“样板”和约束条件**,具体怎么将寄存器与变量结合上去完全由GCC和GAS负责。具体而言就是:在指令部,加上前缀%的数字(如%0,%1)就是须要使用寄存器的**“样板”操作数。指令部中使用几个样板操作数,就表明有几个变量须要与寄存器相结合,这样GCC和GAS在编译和汇编时会依照前面给定的约束条件**进行恰当的处理。因为样板操作数也使用%作为前缀,因而寄存器名后面应当加上两个%,以免形成混淆。紧随在指令部前面的是输出部,是规定输出变量怎样与样板操作数进行结合的条件,每位条件称为一个“约束”,必要时可以包含多个约束,互相之间用冒号分隔开就可以。每位输出约束都以’=‘号开始,之后紧随一个对操作数类型进行说明的字后,最后是怎么与变量相结合的约束。但凡与输出部中说明的操作数相结合的寄存器或操作数本身,在执行完嵌入的汇编代码后均不保留执行之前的内容,这是GCC在调度寄存器时所使用的根据。输出部旁边是输入部,输入约束与输出约束相像,但不带’='号。假如一个输入约束要求使用寄存器,则GCC在预处理时才会为之分配一个寄存器,并插入必要的指令将操作数放入该寄存器。与输入部中说明的操作数结合的寄存器或操作数本身,在执行完嵌入的汇编代码后也不保留执行之前的内容。在内联汇编中用到的操作数从输出部的第一个约束开始编号,序号从0开始,每位约束计数一次。须要注意的是,内联汇编句子的指令部在引用一个操作数时总是将其作为32位的长字使用,但实际情况可能须要的是字或则字节,因而应当在约束中指明正确的限定符:
约束限定字符 含义
“a” 将输入变量放入eax
“b” 将输入变量放入ebx
“c” 将输入变量放入ecx
“d” 将输入变量放入edx
“S” 将输入变量放入esi
“D” 将输入变量放入edi
“q” 将输入变量放入eax,ebx ,ecx ,edx中的一个
“r” 将输入变量放入通用寄存器,也就是eax ,ebx,ecx,edx,esi,edi中的一个
“A” 放入eax和edx,把eax和edx,合成一个64位的寄存器(uselong longs)
“m” 内存变量
“o” 操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
“V” 操作数为内存变量,但寻址方式不是偏移量类型
“,” 操作数为内存变量,但寻址方式为自动增量
“p” 操作数是一个合法的内存地址(指针)
“g” 将输入变量放入eax,ebx,ecx ,edx中的一个或者作为内存变量
“X” 操作数可以是任何类型
“I” 0-31 之间的立即数(用于32位移位指令)
“J” 0-63 之间的立即数(用于64 位移位指令)
“N” 0-255 ,之间的立即数(用于out 指令)
“i” 立即数
“n” 立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”
“=” 操作数在指令中是只写的(输出操作数)
“+” 操作数在指令中是读写类型的(输入输出操作数)
“f” 浮点数
“t” 第一个浮点寄存器
“u” 第二个浮点寄存器
“G” 标准的80387
% 该操作数可以和下一个操作数交换位置
实验具体流程
这儿参考如下链接以及蓝桥云指南即可:
%257B%2522request%255Fid%2522%253A%252268%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=68&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-122268321-null-null.142v88control,239v2insert_chatgpt&utm_term=%E5%93%88%E5%B7%A5%E5%A4%A7%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AE%9E%E9%AA%8C&spm=1018.2226.3001.4187
有一些细节问题注意即可:
当出现这些问题时,须要步入bochs虚拟机(注意不是你的Linux虚拟机)中更改/usr/include/unistd.h中的调用设置调用号
#define __NR_whoami 72
#define __NR_iam 73
好多时侯出现xxx.c:EMOENT通常是你的文件并不存在造成的。
整体实现过程比较简单,这儿就不具体讲了。