码迷,mamicode.com
首页 > 其他好文 > 详细

深入理解系统调用

时间:2020-05-27 01:05:56      阅读:57      评论:0      收藏:0      [点我收藏+]

标签:download   swap   share   内存   虚拟机   加载   vol   代码   ted   

1.实验要求

  • 找一个系统调用,系统调用号为学号最后2位相同的系统调用

  • 通过汇编指令触发该系统调用

  • 通过gdb跟踪该系统调用的内核处理过程

  • 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化

2.选择系统调用

本人学号为342,因此在arch > x86 > entry > syscalls > syscall_64.tbl中找到第42号系统调用为

42 common connect __x64_sys_connect

即为connect系统调用。其内核实现如下:

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;
	err = move_addr_to_kernel(uservaddr, addrlen, &address);
	if (err < 0)
		goto out_put;

	err =
	    security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
	if (err)
		goto out_put;

	err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->file->f_flags);
out_put:
	fput_light(sock->file, fput_needed);
out:
	return err;
}

需要传递三个参数,分别为:本机 socket fd, 对端要连接 socket 的地址 uservaddr,以及地址长度addrlen

3.触发connect调用

connect系统调用用于建立与指定socket的连接。

使用C库函数connect()触发系统调用,代码如下:

// client.c
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define PORT 1234
#define SERVER_IP "127.0.0.1"
int main()
{
int s;
struct sockaddr_in addr;
char buffer[256];
if((s = socket(AF_INET,SOCK_STREAM,0))<0){
	perror("socket");
	exit(1);
}
/* 填写sockaddr_in结构*/
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port=htons(PORT);
addr.sin_addr.s_addr = inet_addr(SERVER_IP);
/* 尝试连线*/
if(connect(s,&addr,sizeof(addr))<0){
	perror("connect");
	exit(1);
}
/* 接收由server端传来的信息*/
recv(s,buffer,sizeof(buffer),0);
printf("%s\n",buffer);
while(1){
	//bzero(buffer,sizeof(buffer));
	memset(buffer,0,sizeof(buffer));
	/* 从标准输入设备取得字符串*/
	read(STDIN_FILENO,buffer,sizeof(buffer));
	/* 将字符串传给server端*/
	if(send(s,buffer,sizeof(buffer),0)<0){
		perror("send");
		exit(1);
	}
}
}


// server.c
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define PORT 1234
#define MAXSOCKFD 10
int main()
{
int sockfd,newsockfd,is_connected[MAXSOCKFD],fd;
struct sockaddr_in addr;
int addr_len = sizeof(struct sockaddr_in);
fd_set readfds;
char buffer[256];
char msg[ ] ="Welcome to server!";
if ((sockfd = socket(AF_INET,SOCK_STREAM,0))<0){
	perror("socket");
	exit(1);
}
memset(&addr,0,sizeof(addr));
addr.sin_family =AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd,&addr,sizeof(addr))<0){
	perror("connect");
	exit(1);
}
if(listen(sockfd,3)<0){
	perror("listen");
	exit(1);
}
for(fd=0;fd<MAXSOCKFD;fd++)
is_connected[fd]=0;
while(1){
	FD_ZERO(&readfds);
	FD_SET(sockfd,&readfds);
	for(fd=0;fd<MAXSOCKFD;fd++)
	if(is_connected[fd]) FD_SET(fd,&readfds);
	if(!select(MAXSOCKFD,&readfds,NULL,NULL,NULL))continue;
	for(fd=0;fd<MAXSOCKFD;fd++)
	if(FD_ISSET(fd,&readfds)){
		if(sockfd ==fd){
			if((newsockfd = accept (sockfd,&addr,&addr_len))<0)
				perror("accept");
			write(newsockfd,msg,sizeof(msg));
			is_connected[newsockfd] =1;
			printf("cnnect from %s\n",inet_ntoa(addr.sin_addr));
		}else{
			memset(buffer,0,sizeof(buffer));
			if(read(fd,buffer,sizeof(buffer))<=0){
				printf("connect closed.\n");
				is_connected[fd]=0;
				close(fd);
			}else
				printf("%s",buffer);
		}
	}
}
}

该客户端与服务端的代码,实现的效果如下:

技术图片

即客户端与服务端都使用地址127.0.0.1,服务端可以接受从客户端发来的字符串并输出打印。


为了观察汇编指令是如何触发系统调用,我们对client进行反汇编objdump -S client > client.S,其中关键代码如下:

  400bdf:	89 85 e4 fe ff ff    	mov    %eax,-0x11c(%rbp)
  400be5:	48 8d 8d e0 fe ff ff 	lea    -0x120(%rbp),%rcx
  400bec:	8b 85 dc fe ff ff    	mov    -0x124(%rbp),%eax
  400bf2:	ba 10 00 00 00       	mov    $0x10,%edx
  400bf7:	48 89 ce             	mov    %rcx,%rsi
  400bfa:	89 c7                	mov    %eax,%edi
  400bfc:	e8 5f b5 04 00       	callq  44c160 <__libc_connect>
      
      
  44c168:	75 16                	jne    44c180 <__libc_connect+0x20>
  44c16a:	b8 2a 00 00 00       	mov    $0x2a,%eax
  44c16f:	0f 05                	syscall 
  44c171:	48 3d 00 f0 ff ff    	cmp    $0xfffffffffffff000,%rax
  44c177:	77 57                	ja     44c1d0 <__libc_connect+0x70>

64位x86体系结构下普通的函数调用和系统调用都是通过寄存器传递参数,RDIRSIRDXRCXR8R9这6个寄存器用作函数/系统调用参数传递,依次对应第 1 参数到第 6 个参数,并且使用EAX传递系统调用号。对应上述汇编代码可以清楚地看到往RDIRSIRDXEAX这四个寄存器中存放数据,往EAX寄存器中存放系统调用号$0x2a。(其中RDXRDI只使用了低32位)


为了通过汇编指令触发该系统调用,我们需要改写client中关于connect()函数地部分,将其改为嵌入式汇编,代码如下:

asm volatile(
        "movl %1,%%edi\n\t"
        "movq %2,%%rsi\n\t"
        "movl %3,%%edx\n\t"
        "movl $0x2a,%%eax\n\t"
        "syscall\n\t"
        "movq %%rax,%0\n\t"
        :"=m"(ret)
        :"m"(s),"g"(&addr),"g"(sizeof(addr))
);

这段嵌入式主要做的事情,就是把相应的参数传到用于传参的寄存器中,并传递系统系统调用号,然后使用syscall进行快速系统调用。

这里遇到了一个问题,花了很长时间才解决,但是仍然不能理解原因:我一开始在写嵌入式汇编时,对于待输入的参数保存,我使用的是:"g"(s),"g"(&addr),"g"(sizeof(addr)),这里g指的是任意一个寄存器,我本来以为这应该是想当然的,但是这样写导致的结果是,客户端可以与服务端进行连接,但是既不能接收服务端发来的信息,也不能向服务端发送信息。后来经过调试,终于发现问题所在:我代码中的s是用来保存socket的文件标识符,这个s也在调用connectsendwrite时是一个待传入的参数,但问题是在这段嵌入式汇编的前后,s的值发生了变化,导致s不能正常指向socket,所以出现了无法发送与接收数据的情况。解决方案是s的值不存到寄存器,而使用:"m"(s),"g"(&addr),"g"(sizeof(addr))来指向待输入的参数。

虽然bug解决了,但是其中的原理我还是没有搞懂,为什么将s的值存到寄存器中,会导致s值的变化,并且我也尝试了指定特定的寄存器去保存s,问题还是存在。


4.gdb跟踪connect调用的内核处理过程

4.1环境配置的步骤如下:

  • 配置内核选项(下载内核见上篇):

    make defconfig # Default configuration is based on ‘x86_64_defconfig‘ 
    make menuconfig # 打开debug相关选项
    	?Kernel hacking  ---> 
    		?Compile-time checks and compiler options  ---> 
    			?[*] Compile the kernel with debug info 
    			?[*]   Provide GDB scripts for kernel debugging
       	?[*] Kernel debugging # 关闭KASLR,否则会导致打断点失败
       		?Processor type and features ----> 
       			?[] Randomize the address of the kernel image (KASLR)
    make -j$(nproc) # 编译
    qemu-system-x86_64 -kernel arch/x86/boot/bzImage #测试内核能不能正常加载运行
    
  • 制作内存根文件系统:

    axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2 
    tar -jxvf busybox-1.31.1.tar.bz2 
    cd busybox-1.31.1
    make menuconfig
    	?Settings  ---> 
    		?[*] Build static binary (no shared libs)
    make -j$(nproc) && make install
    mkdir rootfs 
    cd rootfs 
    cp ../busybox-1.31.1/_install/* ./ -rf 
    mkdir dev proc sys home 
    sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
    
  • 准备其他文件:

    #!/bin/sh 
    # init
    mount -t proc none /proc 
    mount -t sysfs none /sys 
    echo "Wellcome MengningOS!" 
    echo "--------------------" 
    cd home 
    /bin/sh
    

    该初始化文件,用于在启动系统后可以正常加载shell窗口。该文件放于rootfs/init目录下。

    此外,将client-at&t.c文件(使用嵌入式汇编的客户端代码),通过gcc client-at\&t.c -o client-at\&t -static命令编译得到可执行文件,放于rootfs/home目录下。

  • 打包成内存根文件系统镜像

    find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
    
  • 加载根文件系统

    qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
    

加载系统成功如下:

技术图片


4.2gdb调试

  • 首先将通过命令qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s,使得虚拟机启动后即暂停,便于我们调试。

  • 在新开终端中开启gdb调试,我们先在__x64_sys_connect函数处断点

    cd linux-5.4.34/ 
    gdb vmlinux 
    (gdb) target remote:1234 
    (gdb) b __x64_sys_connect
    (gdb) c
    

    可以看到效果如下:

    技术图片


程序执行过程中成功在指定位置暂停下来。

查看断点位置的函数:

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
		int, addrlen)
{
	return __sys_connect(fd, uservaddr, addrlen);
}

该函数即调用在该文一开始就列出的__sys_connect()函数。


此处又存在一个问题:即上述提到的Socket服务端程序与该虚拟机不能并存。

若先开启虚拟机,则运行Socket服务端程序,会在bind()函数调用中出错,错误信息为”Address already in use“,显然是因为Socket服务端不能使用127.0.0.1这个地址与Socket客户端进行连接(虚拟机已占用)。

若先开启Socket服务端程序,虽然虚拟机仍然可以正常启动,但是我没有办法确定虚拟机ip地址,也就无法再使用gdb的target remote ip:port 命令,也就无法实现断点调试功能。

综上,所以放在虚拟机中的client-at&t文件在执行过程中无法连接到Socket服务端,但是对connect的系统调用不受影响,所以实验仍然可以继续进行。

4.3gdb跟踪内核处理过程

当上述Socket客户端程序执行到断点时,在gdb中输入bt来列出调用栈。

调用栈信息为:

#0  __x64_sys_connect (regs=0xffffc900001b7f58) at net/socket.c:1836
#1  0xffffffff81002523 in do_syscall_64 (nr=<optimized out>, 
    regs=0xffffc900001b7f58) at arch/x86/entry/common.c:290
#2  0xffffffff81c0007c in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:175
#3  0x0000000000000000 in ?? ()

可以看出,整个程序系统调用的过程为entry_SYSCALL_64 () -> do_syscall_64() -> __x64_sys_connect(),因此我们在entry_SYSCALL_64 ()do_syscall_64()中也打上断点,然后重新执行程序来跟踪。


运行至断点entry_SYSCALL_64

(gdb) c
Continuing.

Breakpoint 1, entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:153
153		swapgs
(gdb) n
155		movq	%rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
(gdb) n
156		SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
(gdb) n
157		movq	PER_CPU_VAR(cpu_current_top_of_stack), %rsp
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:160
160		pushq	$__USER_DS				/* pt_regs->ss */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:161
161		pushq	PER_CPU_VAR(cpu_tss_rw + TSS_sp2)	/* pt_regs->sp */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:162
162		pushq	%r11					/* pt_regs->flags */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:163
163		pushq	$__USER_CS				/* pt_regs->cs */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:164
164		pushq	%rcx					/* pt_regs->ip */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:166
166		pushq	%rax					/* pt_regs->orig_ax */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:168
168		PUSH_AND_CLEAR_REGS rax=$-ENOSYS
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:173
173		movq	%rax, %rdi
(gdb) n
174		movq	%rsp, %rsi
(gdb) n
175		call	do_syscall_64		/* returns with IRQs disabled */

由于64位系统采用的是快速系统调用,所以系统会自动将 rip 保存到 rcx ,然后将entry_SYSCALL_64加载到 rip。但也是因为采用了快速系统调用,所以在entry_SYSCALL_64中需要手动做一些必要的压栈来保存cpu的关键上下文,也就是上述155行之后的一些代码。同时64位系统的系统调用还引入了swapgs用于保存现场和恢复现场,也就是上述153行代码。

当系统调用完成时,会执行

SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
popq	%rdi
popq	%rsp
USERGS_SYSRET64

来完成内核态到用户态的转变,并实现程序堆栈的切换。


entry_SYSCALL_64在保存完环境后就调用do_syscall_64进行实际的系统调用:

(为了节约篇幅省去了gdb单步调试的结果)

__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
	struct thread_info *ti;

	enter_from_user_mode();
	local_irq_enable();
	ti = current_thread_info();
	if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
		nr = syscall_trace_enter(regs);

	if (likely(nr < NR_syscalls)) {
		nr = array_index_nospec(nr, NR_syscalls);
		regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
	} else if (likely((nr & __X32_SYSCALL_BIT) &&
			  (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
		nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
					X32_NR_syscalls);
		regs->ax = x32_sys_call_table[nr](regs);
#endif
	}

	syscall_return_slowpath(regs);
}
#endif

其中,用于区分不同系统调用的代码为:

regs->ax = sys_call_table[nr](regs);

这一句代码使得系统可以正确调用内核处理函数,也就是我们使用的__x64_sys_connect系统调用。

此外,do_syscall_64的最后还有一句用于进程调度时机处理的代码:

syscall_return_slowpath(regs);

用来保障进程切换的正确进行。


do_syscall_64根据系统调用号,从系统调用表中选择系统调用__x64_sys_connect执行:

函数具体的代码在文章一开始就列出,这里不再详述。


综上,系统调用的过程可以总结如下:

  1. syscall指令

  2. 系统调用处理入口entry_SYSCALL_64

    • 保存现场

    • do_syscall_64

      • 系统调用内核函数组成的sys_call_table数组
      • 进程调度时机syscall_return_slowpath
    • 恢复现场

  3. 系统调用返回sysret

  4. 继续执行syscall指令的下一条指令

此外,需要注意的一点是,上述过程,在保存现场与系统调用返回之间,属于内核态,之外属于用户态。



深入理解系统调用

标签:download   swap   share   内存   虚拟机   加载   vol   代码   ted   

原文地址:https://www.cnblogs.com/winkkk/p/linux_2020_lab2.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!