作业要求:
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
系统调用的存在,有以下重要的意义:
1)用户程序通过系统调用来使用硬件,而不用关心具体的硬件设备,这样大大简化了用户程序的开发。
比如:用户程序通过write()系统调用就可以将数据写入文件,而不必关心文件是在磁盘上还是软盘上,或者其他存储上。
2)系统调用使得用户程序有更好的可移植性。
只要操作系统提供的系统调用接口相同,用户程序就可在不用修改的情况下,从一个系统迁移到另一个操作系统。
3)系统调用使得内核能更好的管理用户程序,增强了系统的稳定性。
因为系统调用是内核实现的,内核通过系统调用来控制开放什么功能及什么权限给用户程序。
这样可以避免用户程序不正确的使用硬件设备,从而破坏了其他程序。
4)系统调用有效的分离了用户程序和内核的开发。
用户程序只需关心系统调用API,通过这些API来开发自己的应用,不用关心API的具体实现。
内核则只要关心系统调用API的实现,而不必管它们是被如何调用的。
一、选择系统调用
本人学号尾数为45,打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl
,查看要选择进行实验的系统调用。45号系统调用为recvfrom。
recvfrom的功能是从套接字上接收一个消息。对于recvfrom ,可同时应用于面向连接的和无连接的套接字。recv一般只用在面向连接的套接字,几乎等同于recvfrom,只要将recvfrom的第五个参数设置NULL。当应用程序调用recv函数时,recv先等待s的发送缓冲 中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,如果s的发送缓冲中没有数 据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到 协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中
二、环境准备
首先配置内核选项如下图所示(调试内核必须这样配置):
之后使用busybox制作根文件系统,首先配置busybox使用静态链接
之后使用命令make -j$(nproc) && make install编译安装busybox。默认安装路径为源码目录的_install下面。
再到家目录下面新建rootfs文件夹,将_install目录中的所有文件拷贝过去。并且新建几个目录(dev proc sys home等)和文件(dev/console dev/null dev/tty*)。
准备init脚本⽂件放在根⽂件系统跟⽬录下(rootfs/init),在init⽂件中添加这些内容:
之后给init脚本添加可执⾏权限:chmod +x init
打包成内存根⽂件系统镜像:
启动qemu测试内核启动是否执行init
运行结果如下
三、汇编改写
首先服务器运行在虚拟机上,以下为服务器的代码:
#include <unistd.h> #include <string.h> #include <stdio.h> #include <iostream> #include <netinet/in.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #define MAXLINE 4096 #define UDPPORT 8001 #define SERVERIP "127.0.0.1" using namespace std; int main(){ int serverfd; unsigned int server_addr_length, client_addr_length; char recvline[MAXLINE]; char sendline[MAXLINE]; struct sockaddr_in serveraddr , clientaddr; // 使用函数socket(),生成套接字文件描述符; if( (serverfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){ perror("socket() error"); exit(1); } // 通过struct sockaddr_in 结构设置服务器地址和监听端口; bzero(&serveraddr,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(UDPPORT); server_addr_length = sizeof(serveraddr); // 使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定; if( bind(serverfd, (struct sockaddr *) &serveraddr, server_addr_length) < 0){ perror("bind() error"); exit(1); } // 接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据; client_addr_length = sizeof(sockaddr_in); int recv_length = 0; recv_length = recvfrom(serverfd, recvline, sizeof(recvline), 0, (struct sockaddr *) &clientaddr, &client_addr_length); cout << "recv_length = "<< recv_length <<endl; cout << recvline << endl; // 向客户端发送数据,使用sendto() 函数向服务器主机发送数据; int send_length = 0; sprintf(sendline, "hello client !"); send_length = sendto(serverfd, sendline, sizeof(sendline), 0, (struct sockaddr *) &clientaddr, client_addr_length); if( send_length < 0){ perror("sendto() error"); exit(1); } cout << "send_length = "<< send_length <<endl; //关闭套接字,使用close() 函数释放资源; close(serverfd); return 0; }
接下来是客户端的汇编调用recvfrom系统调用代码如下:
#include <unistd.h> #include <string.h> #include <stdio.h> #include <iostream> #include <netinet/in.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #define MAXLINE 4096 #define UDPPORT 8001 #define SERVERIP "127.0.0.1" using namespace std; int main(){ int confd; unsigned int addr_length; char recvline[MAXLINE]; char sendline[MAXLINE]; struct sockaddr_in serveraddr; // 使用socket(),生成套接字文件描述符; if( (confd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){ perror("socket() error"); exit(1); } //通过struct sockaddr_in 结构设置服务器地址和监听端口; bzero(&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = inet_addr(SERVERIP); serveraddr.sin_port = htons(UDPPORT); addr_length = sizeof(serveraddr); // 向服务器发送数据,sendto() ; int send_length = 0; sprintf(sendline,"hello server!"); send_length = sendto(confd, sendline, sizeof(sendline), 0, (struct sockaddr *) &serveraddr, addr_length); if(send_length < 0 ){ perror("sendto() error"); exit(1); } cout << "send_length = " << send_length << endl; // 接收服务器的数据,recvfrom() ; int recv_length = 0; // recv_length = recvfrom(confd, recvline, sizeof(recvline), 0, (struct sockaddr *) &serveraddr, &addr_length); asm volatile( "movq %1, %%rdi " "movq %2, %%rsi " "movq %3, %%rdx " "movq $0x0, %%rcx " "movq %4, %%r8 " "movq %5, %%r9 " "movl $0x2D,%%eax " "syscall " "movq %%rax,%0 " :"=m"(recv_length) :"m"(confd), "p"(recvline), "X"(sizeof(recvline)), "p"((struct sockaddr *) &serveraddr), "p"(&addr_length) ); cout << "recv_length = " << recv_length <<endl; cout << recvline << endl; // 关闭套接字,close() ; close(confd); return 0; }
接下来使用
使用gcc运行代码如下(因为client运行在内核上 所以使用静态方式):
g++ server.cpp -o server g++ client.cpp -o client -static
运行结果如下:
四、gdb调试与分析
重新打包根文件目录,纯命令⾏下启动虚拟机
qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
此时虚拟机会暂停在启动界面。在另一个terminal中开启gdb调试 gdb vmlinux
,连接进行调试,target remote:1234。结果如下图所示:
如下图打上断点:
如下图所示,由于qemu无法使用网络,客户端和服务器端无法进行通信,也就无法进行下一步的操作
虽然无法继续调试但是可以接着分析下recvfrom函数的调用。在上面的调用图中可以看见该函数位于net/socket.c文件下的第2023行
__sys_recvfrom函数如下图:
首先调用import_single_range,在import_single_range中,MAX_RW_COUNT是一个宏:INT_MAX & PAGE_MASK,INT_MAX是2^31,理论上每次write可写的buff大小是2^31-2^12=2147479552,然后使用了迭代器
int import_single_range(int rw, void __user *buf, size_t len, struct iovec *iov, struct iov_iter *i) { if (len > MAX_RW_COUNT) len = MAX_RW_COUNT; if (unlikely(!access_ok(!rw, buf, len))) return -EFAULT; iov->iov_base = buf; iov->iov_len = len; iov_iter_init(i, rw, iov, 1, len); return 0; } EXPORT_SYMBOL(import_single_range);
接下来用通过sockfd_lookup_light()来通过文件描述符fd来找到我们需要的结构体
static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed) { struct file *file; struct socket *sock; *err = -EBADF; file = fget_light(fd, fput_needed);//根据fd获取file结构体 if (file) { sock = sock_from_file(file, err);//根据file结构体获取socket结构体 if (sock) return sock; fput_light(file, *fput_needed); } return NULL; }
可见,主要分成两个部分,一个是由fd找到file结构体,然后才是由file结构体获取socket结构体。
先看看是如何从fd找到file结构体的。
struct file *fget_light(unsigned int fd, int *fput_needed) { struct file *file; struct files_struct *files = current->files;//获取当前进程打开的文件列表 *fput_needed = 0; //如果只有一个进程在使用,那就不需要加锁了,锁比较耗性能 if (atomic_read(&files->count) == 1) { file = fcheck_files(files, fd);//根据files_struct结构获取file结构体 if (file && (file->f_mode & FMODE_PATH)) file = NULL; } else { rcu_read_lock();//多个进程使用,需要加锁保护 file = fcheck_files(files, fd); if (file) { if (!(file->f_mode & FMODE_PATH) && atomic_long_inc_not_zero(&file->f_count)) *fput_needed = 1; else /* Didn't get the reference, someone's freed */ file = NULL; } rcu_read_unlock(); } return file; } static inline struct file * fcheck_files(struct files_struct *files, unsigned int fd) { struct file * file = NULL; struct fdtable *fdt = files_fdtable(files);//获得文件描述符位图表 if (fd < fdt->max_fds) //根据句柄fd获取file结构体,fdt->fd可以理解为一个数组,以文件句柄fd为索引 file = rcu_dereference_check_fdtable(files, fdt->fd[fd]); return file; }
由此可见,进程结构体task_struct维护了一个files_struct结构体,用于记录当前进程使用的文件情况,这样也便于控制每个进程允许打开的文件个数,但这个就是另外的话题了。files_struct结构体里的fdtable变量里存放了该进程使用的所有文件句柄,并且每个文件句柄关联到了对应的file结构体。因此以fd为索引就能获取file结构体。这个赋值操作是在socket()系统调用做的,通过fd_install()函数完成fd和file结构体的关联。
最后调用sock_recvmsg函数接收信息,如果无误就使用move_addr_to_user传递给用户,函数结尾的使用调用 fput_light() 更新文件的引用计数。
五、系统调用栈空间分析
在具有函数调用的处理例程当中,各个函数的栈空间关系如下图所示:
在所有的寄存器中,%rax 通常用于存储函数调用的返回结果,%rsp 是堆栈指针寄存器,它会一直指向栈顶位置,堆栈的pop和push操作就是通过改变%rsp 的值即移动堆栈指针的位置来实现出栈和压栈操作的。%rbp 是栈帧指针,用于标识当前栈帧的起始位置,剩余的%rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数。
在整个系统调用过程中,调用方主要做的操作有:
1、先把参数保存在寄存器edi和esi中(通过寄存器传参数)
2、调用callq,其中callq保存下一条指令的地址,用于函数返回继续执行,之后再跳转到子函数地址。
3、处理返回值,函数返回值通常存放在%eax。