深入理解系统调用
作业要求:
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
一、学号对应系统调用
查找系统调用表,发现70 对应系统调用,为setreuid
setruid作用于ruid 与euid,与其作用相近的系统调用还有setuid 和 seteuid。分别解释其作用:
int setuid(uid_t uid)
1) 若进程具有超级用户权限,则setuid将实际用户ID、有效用户ID及保存的设置用户ID设置为uid
2) 若进程没有超级用户权限,但是uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid
int seteuid(uid_t uid)
1) 若进程具有超级用户权限,则setuid只将有效用户ID设置为uid
2) 若进程没有超级用户权限,则setuid只将有效用户ID设置为uid, 但是uid必须等于实际用户ID或保存的设置用户ID,
int setreuid(uid_t ruid, uid_t euid)
1) 针对设置用户ID位的程序: 交换有效用户ID和保存的设置用户ID
2) 针对没有设置用户ID位的程序: 交换有效用户ID和实际用户ID
更直观地,用图来表示三者的关系。
setreuid(0,0)系统调用主要用来交换Linux进程的实际用户ID和有效用户ID。每个Linux进程都有有两个相关的用户ID:实际用户ID(即ruid)和有效用户ID(即euid),其中ruid表示了该进程由谁运行,即当前系统环境用户是谁,主要回答who am I?的问题;而euid则用来规范进程的实际权限控制。比如passwd文件存放了用户名和密码,当一个普通用户运行passwd时,其ruid是自己,而euid则临时变为了文件的所有者root。这主要是设置SUID来实现的,而setreuid的作用在于交换ruid和euid;
2.编写以下两个文件setreuid.c和setreuidAsm.c,比较系统API和汇编调执行结果
/*setreuid.c*/ #include<unistd.h> #include<stdio.h> int main(void){ int i = 0, j=0,k = 0,m=0; i = geteuid(); j=getuid(); printf("curent euid is:%d,curent uid is:%d\n", i,j); setreuid(j,i); k = geteuid(); m=getuid(); printf("after change euid:%d,after change uid:%d\n", k,m); return 0;
}
/* setreuidAsm.c */ #include<unistd.h> #include<unistd.h> #include<stdio.h> int main(void){ int i = 0, j = 0, k = 0,m=0,f=0; asm volatile( "mov $0,%%ebx\n\t" "mov $0x18,%%eax\n\t" /* 24号系统调用getuid */ "int $0x80\n\t" "mov %%eax,%0\n\t" :"=m"(i) ); asm volatile( "mov $0,%%ebx\n\t" "mov $0x6B,%%eax\n\t" /* 107号系统调用geteuid */ "int $0x80\n\t" "mov %%eax,%0\n\t" :"=m"(j) ); printf("current ruid is:%d,current euid is :%d\n", i,j); asm volatile( "mov $0,%%ebx\n\t" "mov $1,%%ecx\n\t" "mov $0x46,%%eax\n\t" /* setreuid */ "int $0x80\n\t" "mov %%eax,%4\n\t" :"=m"(f) ); asm volatile( "mov $2,%%ebx\n\t" "mov $0x18,%%eax\n\t" /* 24号系统调用getuid */ "int $0x80\n\t" "mov %%eax,%2\n\t" :"=m"(k) ); asm volatile( "mov $3,%%ebx\n\t" "mov $0x6B,%%eax\n\t" /* 107号系统调用geteuid */ "int $0x80\n\t" "mov %%eax,%3\n\t" :"=m"(m) ); printf("aftr change uid is:%d,after change euid is :%d\n",k,m); return 0; }
编译这块就是查到对应的系统调用号装入eax里然后进入系统调用,返回值也在eax中。进入系统调用的参数可以按次序传入ebx、ecx中。
最后使用 gcc -o setreuid setreuid.c 命令编译后,运行;使用 gcc -o setreuidasm setreuidasm.c 命令编译后,运行。两程序运行结果相同。
(不知道怎么修改这两种id,因为两者一样,交换看不出区别)
gdb的环境出了问题,总会无视断点,还在找问题中。参照其他的博客观察gdb设置断点然后单步调试过程,可以归纳出整个调用过程,寄存器eax保存系统调用号70、ebx和ecx保存栈顶参数;调用中断指令int 0x80进入内核态;linux-5.4.34/arch/x86/entry/entry_64.S 目录下的ENTRY(entry_SYSCALL_64)入口,然后开始通过swapgs 和压栈动作保存现场。取到系统调用号之后执行system_call(),此时根据eax查询系统调用表,找到服务地址,执行服务;服务结束后,恢复保存的现场,回到用户态,ret返回结果
对于system_call()的过程,其源码如下:
ENTRY(system_call) RING0_INT_FRAME # can't unwind into user space anyway ASM_CLAC pushl_cfi %eax # save orig_eax SAVE_ALL GET_THREAD_INFO(%ebp) # system call tracing in operation / emulation testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) jnz syscall_trace_entry cmpl $(NR_syscalls), %eax jae syscall_badsys # 查找系统调用表 syscall_call: call *sys_call_table(,%eax,4) syscall_after_call: movl %eax,PT_EAX(%esp) # store the return value syscall_exit: LOCKDEP_SYS_EXIT DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt # setting need_resched or sigpending # between sampling and the iret TRACE_IRQS_OFF movl TI_flags(%ebp), %ecx testl $_TIF_ALLWORK_MASK, %ecx # current->work # 系统调用执行完后,进入。若没有进入,进行恢复现场工作 jne syscall_exit_work restore_all: TRACE_IRQS_IRET restore_all_notrace: #ifdef CONFIG_X86_ESPFIX32 movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS # Warning: PT_OLDSS(%esp) contains the wrong/random values if we # are returning to the kernel. # See comments in process.c:copy_thread() for details. movb PT_OLDSS(%esp), %ah movb PT_CS(%esp), %al andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax CFI_REMEMBER_STATE je ldt_ss # returning to user-space with LDT SS #endif restore_nocheck: RESTORE_REGS 4 # skip orig_eax/error_code # 效果等同与iret, 返回到用户态程序继续执行 irq_return: INTERRUPT_RETURN
具体分析可见《庖丁解牛Linux内核分析》 第五章第三节。