一、实验要求
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用;
- 通过汇编指令触发该系统调用;
- 通过gdb跟踪该系统调用的内核处理过程;
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化。
二、实验目的
- 理解Linux操作系统调用;
- 了解系统调用过程中内核堆栈状态的变化过程。
三、实验环境
ubuntu-16.04.6(实验楼环境:https://www.shiyanlou.com/courses/195/learning/?id=725)
四、实验相关
1、用户态与内核态的切换
内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
-------------------------------
1)OS采用系统调用实现用户态进程与I/O进行交互,用户态下调用系统资源须采用系统调用。
2)从用户态进入内核态有2种方式:系统调用(trap陷入)、中断。
3)状态切换时会保存寄存器上下文,如用户态堆栈顶地址、当时的状态字、当时的cs:eip值。
4)system_ call是linux中所有系统调用的入口点,系统调用的参数由eax传递
五、实验步骤
1、根据学号选择系统调用
进入根目录下,通过 find . -name 'syscall_64.tbl' 查找系统调用表,我的学号尾号为82,因此查表之后,选择的系统调用为 rename 。
此函数的系统调用函数的入口函数为sys_rename通过搜索,入口函数位置为/fs/namei.c。
rename系统调用用于在同一个文件系统中做文件的rename操作。如果源和目的在不同mount点上,rename会返回错误EXDEV。
SYSCALL_DEFINE3(symlinkat, const char __user *, oldname, int, newdfd, const char __user *, newname) { int error; struct filename *from; struct dentry *dentry; struct path path; unsigned int lookup_flags = 0; from = getname(oldname); if (IS_ERR(from)) return PTR_ERR(from); retry: dentry = user_path_create(newdfd, newname, &path, lookup_flags); error = PTR_ERR(dentry); if (IS_ERR(dentry)) goto out_putname; error = security_path_symlink(&path, dentry, from->name); if (!error) error = vfs_symlink(path.dentry->d_inode, dentry, from->name); done_path_create(&path, dentry); if (retry_estale(error, lookup_flags)) { lookup_flags |= LOOKUP_REVAL; goto retry; } out_putname: putname(from); return error; } SYSCALL_DEFINE2(symlink, const char __user *, oldname, const char __user *, newname) { return sys_symlinkat(oldname, AT_FDCWD, newname); }
SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname)
{
return sys_renameat2(AT_FDCWD, oldname, AT_FDCWD, newname, 0);
}
可以看到,它实际上是转调用了renameat系统调用。renameat系统调用的实现也在./fs/namei.c中,它的函数定义是:
SYSCALL_DEFINE4(renameat, int, olddfd, const char __user *, oldname, int, newdfd, const char __user *, newname);
下面是它的具体实现:
1、对oldname和newname分别做目录查找,得到它们对应的nameidata数据结构oldnd和newnd。这个过程会涉及到查找目录项缓存,如果目录不在目录项缓存中,需要将目录从磁盘读取到目录项缓存中,具体细节见这里: http://www.cnblogs.com/cobbliu/p/4888751.html。
2、查看oldnd和newnd的mount点是否一样,不一样则返回EXDEV
3、做一堆其他的验证和准备工作,这个过程中会找到oldname的old_dir的inode和old_dentry,newname的new_dir的inode和new_dentry
4、调用VFS层的 error = vfs_rename(old_dir->d_inode, old_dentry, new_dir->d_inode, new_dentry) ;
int vfs_rename(struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry) 的实现:
1、如果目的和源的inode一样,则返回0
2、查看是否需要删除old_dentry和是否需要新建new_dentry
3、如果old_dentry是个目录则调用vfs_rename_dir,否则调用vfs_rename_other
static int vfs_rename_other(struct inode *old_dir, struct dentry *old_dentry,struct inode *new_dir, struct dentry *new_dentry) 的实现:
1、调用 dget(new_dentry)
2、调用 old_dir->i_op->rename(old_dir, old_dentry, new_dir, new_dentry); 做真正的rename操作
3、调用 dput(new_dentry)
ext4_rename函数真正实现了rename过程,ext4_rename实际上是讲旧目录文件中的文件项的refcount递减,然后在新目录文件中加入新文件名的目录项,并不会移动实际的数据文件,也不会修改数据文件的inode号。
/proc/sys/fs/dentry_state显示目录项高速缓存的一些信息:
- nr_dentry - number of dentries currently allocated
- nr_unused - nuber of unused dentries
- age_limit - seconds after the entry may be reclaimed, when memory is short
- remaining - reserved.
通常linux文件系统中目录项高速缓存的age_limit是45s,也就是说该目录项在目录项高速缓存中停留45s还无访问,就将它换出。
2、触发系统调用
使用下面的代码触发rename系统调用:
#include<stdio.h> #include <fcntl.h> int main(void) { char oldname[100], newname[100]; /* 触发rename文件重命名的系统调用 */ // 传入需要修改文件名的完整路径 printf("enter a entire file path: "); gets(oldname); // 传入新文件的名字 printf("new path: "); gets(newname); // 调用rename if (rename(oldname, newname) == 0) printf("change %s to %s.\n", oldname, newname); else perror("rename"); return 0; }
创建一个新文件命名为oldPath
运行上述代码
gcc main.c -o main ./main
对上述的程序进行修改,使用汇编来调用rename,其实就是使用汇编指令传递name的参数,并使用系统调用通过软中断0x80陷入内核
#include<stdio.h> #include <fcntl.h> int main(void) { char oldname[100], newname[100]; printf("enter a entire file path: "); gets(oldname); printf("new path: "); gets(newname); int flag; asm volatile( "movl $1,%%ebx\n\t" //系统调⽤传递第⼀个参数使⽤EBX寄存器 "movl $2,%%ecx\n\t" "movl $0x52,%%eax\n\t" // 0x52 号系统调用放入eax "int $0x80\n\t" "movl %%eax,%0\n\t" :"=m"(flag) // 接收返回 :"b"(oldname), "c"(newname) ); if (flag == 0) printf("change %s to %s.\n", oldname, newname); else perror("rename"); return 0; }
编译、运行
3、通过gdb跟踪系统调用
重新打包根文件目录,纯命令⾏下启动虚拟机。
qemu-system-x86_64 -kernel shiyanlou/linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
,此时虚拟机会暂停在启动界面。在另一个terminal中开启gdb调试 gdb vmlinux
,连接进行调试,target remote:1234。结果如下图所示:
六、实验总结
1 main: 2 .LFB0: 3 4 pushl %ebp 5 6 movl %esp, %ebp 7 8 andl $-16, %esp 9 subl $224, %esp 10 movl 12(%ebp), %eax 11 movl %eax, 12(%esp) 12 movl %gs:20, %eax 13 movl %eax, 220(%esp) 14 xorl %eax, %eax 15 movl $.LC0, (%esp) 16 call printf 17 leal 20(%esp), %eax 18 movl %eax, (%esp) 19 call gets 20 movl $.LC1, (%esp) 21 call printf 22 leal 120(%esp), %eax 23 movl %eax, (%esp) 24 call gets 25 leal 120(%esp), %eax 26 movl %eax, 4(%esp) 27 leal 20(%esp), %eax 28 movl %eax, (%esp) 29 call rename 30 testl %eax, %eax 31 jne .L2 32 leal 120(%esp), %eax 33 movl %eax, 8(%esp) 34 leal 20(%esp), %eax 35 movl %eax, 4(%esp) 36 movl $.LC2, (%esp) 37 call printf 38 jmp .L3 39 .L2: 40 movl $.LC3, (%esp) 41 call perror 42 .L3: 43 movl $0, %eax 44 movl 220(%esp), %edx 45 xorl %gs:20, %edx 46 je .L5 47 call __stack_chk_fail 48 .L5: 49 leave 50 .cfi_restore 5 51 .cfi_def_cfa 4, 4 52 ret 53 .cfi_endproc
用户空间->内核空间
INT 0x80(封装在C库函数中) -->system_call(系统调用处理程序)-->系统调用服务例程 -->内核程序
系统调用时通过软中断指令INT 0x80实现的,这条指令会让系统跳转到一个预先设置好的内核地址,指向系统调用处理程序system_call。
1.执行int 0x80指令后系统从用户态进入内核态,跳到system_call()函数处执行相应服务进程。在此过程中内核先保存中断环境,然后执行系统调用函数。
2.system_call()函数通过系统调用号查找系统调用表sys_cal_table来查找具体系统调用服务进程。
3.执行完系统调用后,iret之前,内核会检查是否有新的中断产生、是否需要进程切换、是否学要处理其它进程发送过来的信号等。
4.内核是处理各种系统调用的中断集合,通过中断机制实现进程上下文的切换,通过系统调用管理整个计算机软硬件资源。
5.如没有新的中断,restore保存的中断环境并返回用户态完成一个系统调用过程。
七、参考:
实验楼:https://www.shiyanlou.com/courses/195/learning/?id=725
https://blog.csdn.net/bshcc/article/details/50950604