当前位置:   article > 正文

操作系统真象还原实验记录之实验二十:实现系统调用_syscall(sys_getpid)

syscall(sys_getpid)

操作系统真象还原实验记录之实验二十:实现系统调用

Linux系统调用

Linux系统调用本质是
执行宏 _syscall[0~6] (NUMBER, …),(0 ~6是参数个数)
该宏将子功能号传入eax,第一个参数传入ebx,第二个参数传入ecx,第三个参数传入edx,第四个参数传入esi,第五个参数传入edi,然后执行int 0x80中断。
中断向量号就是0x80,对应的中断处理程序中,call [syscall_table + eax * 4]
syscall_table[syscall_nr] 应该提前写好对应的处理函数
比如 syscall_table[SYS_GETPID] =sys_getpid

本次实验下,内联汇编引发的int 80中断的宏,被封装在c函数里,这就和c语言库函数实现系统调用是类似的。

本次实验,用户进程在特权级3下调用getpid(),调用宏_syscall0(SYS_GETPID)内联汇编进入中断
中断处理程序调用syscall_table[SYS_GETPID],即sys_getpid()获得PCB->pid返回。
根据二进制接口abi,c函数返回值会存入eax寄存器,但是sys_getpid()是在内核下完成的,返回值会存入内核态的eax,中断结束,会将0特权级栈保存用户进程的环境全部pop,所以将eax覆盖0特权级栈的eax处,这样恢复环境后,pid就能成功回到用户态的那句内联汇编,eax输入到c变量

mov [esp + 8 * 4], eax
为什么是8 * 4?
宏_syscall0(NUMBER)是无参数的,只有一个子功能号,内联汇编下输入eax
中断处理程序直到pushad才算是把这个eax(内含子功能号)保存下来,然后又入了三个 有add esp, 12,所以显然eax在第8个

1.实验代码

1.1 interrupt.c(增加)

#define IDT_DESC_CNT 0x81      // 目前总共支持的中断数
extern uint32_t syscall_handler(void);


/*初始化中断描述符表*/
static void idt_desc_init(void) {
   int i, lastindex = IDT_DESC_CNT - 1;
   for (i = 0; i < IDT_DESC_CNT; i++) {
      make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); 
   }
/* 单独处理系统调用,系统调用对应的中断门dpl为3,
 * 中断处理程序为单独的syscall_handler */
   make_idt_desc(&idt[lastindex], IDT_DESC_ATTR_DPL3, syscall_handler);
   put_str("   idt_desc_init done\n");
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

1.2 kernel.S


;;;;;;;;;;;;;;;;   0x80号中断   ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境
   push 0			    ; 压入0, 使栈中格式统一

   push ds
   push es
   push fs
   push gs
   pushad			    ; PUSHAD指令压入32位寄存器,其入栈顺序是:
				    ; EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI 
				 
   push 0x80			    ; 此位置压入0x80也是为了保持统一的栈格式

;2 为系统调用子功能传入参数
   push edx			    ; 系统调用中第3个参数
   push ecx			    ; 系统调用中第2个参数
   push ebx			    ; 系统调用中第1个参数

;3 调用子功能处理函数
   call [syscall_table + eax*4]	    ; 编译器会在栈中根据C函数声明匹配正确数量的参数
   add esp, 12			    ; 跨过上面的三个参数

;4 将call调用后的返回值存入待当前内核栈中eax的位置
   mov [esp + 8*4], eax	
   jmp intr_exit		    ; intr_exit返回,恢复上下文


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

1.3 syscall-init.c

#include "syscall.h"

#define syscall_nr 32 
typedef void* syscall;
syscall syscall_table[syscall_nr];

/* 返回当前任务的pid */
uint32_t sys_getpid(void) {
   return running_thread()->pid;
}

/* 初始化系统调用 */
void syscall_init(void) {
   put_str("syscall_init start\n");
   syscall_table[SYS_GETPID]	    = sys_getpid;
   put_str("syscall_init done\n");
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

1.4 syscall-init.h

#ifndef __USERPROG_SYSCALLINIT_H
#define __USERPROG_SYSCALLINIT_H
#include "stdint.h"
void syscall_init(void);
uint32_t sys_getpid(void);
#endif

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

1.5 thread.c

struct lock pid_lock;

/* 分配pid */
static pid_t allocate_pid(void) {
   static pid_t next_pid = 0;
   lock_acquire(&pid_lock);	
   next_pid++;
   lock_release(&pid_lock);
   return next_pid;
}


/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
   memset(pthread, 0, sizeof(*pthread));
   pthread->pid = allocate_pid();
   strcpy(pthread->name, name);

}

/* 初始化线程环境 */
void thread_init(void) {
   put_str("thread_init start\n");

   list_init(&thread_ready_list);
   list_init(&thread_all_list);
 lock_init(&pid_lock);
 
/* 将当前main函数创建为线程 */
   make_main_thread();
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

1.6 thread.h

typedef int16_t pid_t;

struct task_struct{
	pid_t pid;
}
  • 1
  • 2
  • 3
  • 4
  • 5

1.7 syscall.c

#include "syscall.h"

/* 无参数的系统调用 */
#define _syscall0(NUMBER) ({				       \
   int retval;					               \
   asm volatile (					       \
   "int $0x80"						       \
   : "=a" (retval)					       \
   : "a" (NUMBER)					       \
   : "memory"						       \
   );							       \
   retval;						       \
})


/* 一个参数的系统调用 */
#define _syscall1(NUMBER, ARG1) ({			       \
   int retval;					               \
   asm volatile (					       \
   "int $0x80"						       \
   : "=a" (retval)					       \
   : "a" (NUMBER), "b" (ARG1)				       \
   : "memory"						       \
   );							       \
   retval;						       \
})

/* 两个参数的系统调用 */
#define _syscall2(NUMBER, ARG1, ARG2) ({		       \
   int retval;						       \
   asm volatile (					       \
   "int $0x80"						       \
   : "=a" (retval)					       \
   : "a" (NUMBER), "b" (ARG1), "c" (ARG2)		       \
   : "memory"						       \
   );							       \
   retval;						       \
})

/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({		       \
   int retval;						       \
   asm volatile (					       \
      "int $0x80"					       \
      : "=a" (retval)					       \
      : "a" (NUMBER), "b" (ARG1), "c" (ARG2), "d" (ARG3)       \
      : "memory"					       \
   );							       \
   retval;						       \
})

/* 返回当前任务pid */
uint32_t getpid() {
   return _syscall0(SYS_GETPID);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

1.8 syscall.h

#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H
#include "stdint.h"
#include "thread.h"

enum SYSCALL_NR {
   SYS_GETPID,
};
uint32_t getpid(void);
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

1.9 main.c

#include "interrupt.h"
#include "init.h"
#include "thread.h"
#include "print.h"
#include "process.h"
#include "console.h"
#include "syscall.h"
#include "syscall-init.h"


void k_thread_a(void* );
void k_thread_b(void* );
void u_prog_a (void);
void u_prog_b (void);

int prog_a_pid = 0, prog_b_pid=0;

int main(void) {
   put_str("I am kernel\n");
   init_all();
	
	process_execute(u_prog_a, "user_prog_a");
	process_execute(u_prog_b, "user_prog_b");
	
   intr_enable();
   console_put_str(" main_pid:0x");
   console_put_int(sys_getpid());
   console_put_char('\n');
  
	thread_start("k_thread_a", 31, k_thread_a, "argA ");
	thread_start("k_thread_b", 8, k_thread_b, "argB ");
	while(1);
   return 0;
}

void k_thread_a(void* arg){
	char* para = arg;
	console_put_str(" thread_a_pid:0x");
   console_put_int(sys_getpid());
   console_put_char('\n');
	console_put_str(" prog_a_pid:0x");
   console_put_int(prog_a_pid);
   console_put_char('\n');
	while(1);
}

void k_thread_b(void* arg){
	char* para = arg;
	console_put_str(" thread_b_pid:0x");
   console_put_int(sys_getpid());
   console_put_char('\n');
	console_put_str(" prog_b_pid:0x");
   console_put_int(prog_b_pid);
   console_put_char('\n');
   while(1);
}

void u_prog_a(void) {
	prog_a_pid = getpid();
	while(1);
}

void u_prog_b(void) {
	prog_b_pid = getpid();
	while(1);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

用户进程的getpid()才算是系统调用,调用了宏触发中断,有特权级转换。
线程的sys_getpid()只是调用了内核函数而已,但功能一致。

用户进程能否调用sys_getpid()?答案是可以的,只是没有涉及特权级变化。
目前来看,用户进程涉及特权级变化,要么是时钟中断、键盘中断,要么是本次实验的系统调用getpid()内联汇编触发int 80中断,中断意味着要从tss.esp使用0特权级栈。
或者lock被阻塞(??lock代码忘了,不一定,回头再看)
分析如下:

main函数入口地址位于0xc0001500 ,这处于0xc0000000 ~ 0xc0100000即3GB到3GB+1MB的区域,sys_getpid()这个函数也不会离main有多远,所以,编译器给sys_getpid这个标号的地址也在3GB到3GB+1MB的区域,而3GB到4GB的页目录项都复制到每个进程的页目录表了。
其次用户进程在内核cs、ds被成功赋值成RPL=3的选择子,也指向早已被填充好的好的DPL=3的代码段,数据段,所以均会获得为0的段基址,同时DPL=3的代码段,数据段段界限都允许段内偏移地址在4GB,因此eip的寻址范围是4GB,用户态完全可以执行sys_getpid()。
因为我们的os在DPL=3的段描述符下未限制段界限。所以用户态(CPL=3)有3GB以上页表映射,也不会段越界,可以随意调用内核函数。
正常情况下完整的os,内核函数应该被严格限制在DPL=0的段内,其他段的范围都访问不到,那么即使页表映射支持,也会存在越特权级。只有特权级为0的时候才能访问。

2 实验结果

在这里插入图片描述

3. 无法理解的bug

十九、二十这两次实验代码编译好后都出不来结果,报缺页异常。
但是只要随便改一下time.c,可能makefile把他重新编译了一下,就出来了,很莫名其妙搞不懂。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/58694
推荐阅读
相关标签
  

闽ICP备14008679号