深入理解系统调用

Posted yongjason

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解系统调用相关的知识,希望对你有一定的参考价值。

一、实验要求

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

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

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

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

二、实验环境搭建

 2.1安装开发工具

1 sudo apt install build-essential

2 sudo apt install qemu # install QEMU

3 sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev

 2.2下载内核源码

1 sudo apt install axel
2 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz 
3 xz -d linux-5.4.34.tar.xz 
4 tar -xvf linux-5.4.34.tar 
5 cd linux-5.4.34

 2.3配置内核

1 make defcon?g # Default con?guration is based on ‘x86_64_defcon?g‘
2 make menucon?g  
3 Kernel hacking  ---> 
4    Compile-time checks and compiler options  ---> 
5        [*] Compile the kernel with debug info 
6       [*]   Provide GDB scripts for kernel debugging
7 [*] Kernel debugging 
8 Processor type and features ----> 
9   [] Randomize the address of the kernel image (KASLR)

 2.4编译运行内核

1 make -j$(nproc) # nproc gives the number of CPU cores/threads available
2 kernel panic 
3 qemu-system-x86_64 -kernel arch/x86/boot/bzImage

 2.5制作根文件系统

1 首先从https://www.busybox.net下载 busybox源代码解压,解压完成后,跟内核一样先配置编译,并安装。
2 axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2
3 tar -jxvf busybox-1.31.1.tar.bz2
4 cd busybox-1.31.1
1 make menucon?g  #若出现打不开menuconfig,重启虚拟机即可
2 #记得要编译成静态链接,不?动态链接库。
3 Settings  --->
4     [*] Build static binary (no shared libs) 
5 #然后编译安装,默认会安装到源码?录下的 _install ?录中。 
6 make -j$(nproc) && make install
制作根文件系统镜像
1 mkdir rootfs
2 cd rootfs
3 cp ../busybox-1.31.1/_install/* ./ -rf
4 mkdir dev proc sys home
5 sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
准备init脚本文件放在根文件系统跟目录下(rootfs/init),添加如下内容到init文件。 
1 #!/bin/sh
2 mount -t proc none /proc 
3 mount -t sysfs none /sys
4 echo "Wellcome MengningOS!" 
5 echo "--------------------"
6 cd home
7 /bin/sh
给init脚本添加可执?权限
1 chmod +x init
1 #打包成内存根?件系统镜像 
2 ?nd . -print0 | cpio --null -ov --format=newc | gzip -9 > ../ rootfs.cpio.gz 
3 #测试挂载根?件系统,看内核启动完成后是否执?init脚本 
4 qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz

 

三、系统调用

  所有的操作系统在其内核里都有一些内建的函数,这些函数可以用来完成一些系统级别的功能。Linux系统使用的这样的函数叫做“系统调用”,英文是systemcall。这些函数代表了从用户空间到内核空间的一种转换,例如在用户空间调用open函数,则会在内核空间调用sys_open。一个已经安装的系统的支持的所有的系统调用可以在/usr/include/bits/syscall.h文件里面看到。

系统调用是内核向用户进程提供服务的唯一方法,应用程序调用操作系统提供的功能模块(函数)。用户程序通过系统调用从用户态(user mode)切换到核心态(kernel mode ),从而可以访问相应的资源。这样做的好处是:为用户空间提供了一种硬件的抽象接口,使编程更加容易。有利于系统安全。有利于每个进程度运行在虚拟系统中,接口统一有利于移植。

由于学号后两位是52,系统调用号为52的是getpeername

技术图片

 

 #define DEBUG 0

static int _tcp_getpeername(int socket, struct sockaddr *_RESTRICT address,
socklen_t *_RESTRICT address_len, nwio_tcpconf_t *tcpconfp);
int getpeername(int socket, struct sockaddr *_RESTRICT address,
socklen_t *_RESTRICT address_len)
{
int r;
nwio_tcpconf_t tcpconf;
r= ioctl(socket, NWIOGTCPCONF, &tcpconf);
if (r != -1 |e68a84e799bee5baa631333363393630| errno != ENOTTY)
{
if (r == -1)
{
/* Bad file descriptor */
return -1;
}
return _tcp_getpeername(socket, address, address_len,
&tcpconf);
}
#if DEBUG
fprintf(stderr, "getpeername: not implemented for fd %d ", socket);
#endif
errno= ENOSYS;
return -1;
}
static int _tcp_getpeername(int socket, struct sockaddr *_RESTRICT address,
socklen_t *_RESTRICT address_len, nwio_tcpconf_t *tcpconfp)
{
socklen_t len;
struct sockaddr_in sin;
if (tcpconfp->nwtc_remaddr == 0 ||
tcpconfp->nwtc_remport == 0)
{
errno= ENOTCONN;
return -1;
}
memset(&sin, ‘‘, sizeof(sin));
sin.sin_family= AF_INET;
sin.sin_addr.s_addr= tcpconfp->nwtc_remaddr;
sin.sin_port= tcpconfp->nwtc_remport;
len= *address_len;
if (len > sizeof(sin))
len= sizeof(sin);
memcpy(address, &sin, len);
*address_len= len;
return 0;
}
 
getpeername() 
  获取与套接口相连的端地址。
  #include
  int PASCAL FAR getpeername( SOCKET s, struct sockaddr FAR* name,
  int FAR* namelen);
  s:标识一已连接套接口的描述字。
  name:接收端地址的名字结构。
  namelen:一个指向名字结构的指针。
注释:
  getpeername()函数用于从端口s中获取与它捆绑的端口名,并把它存放在sockaddr类型的name结构中。它适用于数据报或流类套接口。
返回值:
  若无错误发生,getpeername()返回0。否则的话,返回SOCKET_ERROR,应用程序可通过WSAGetLastError()来获取相应的错误代码。
错误代码:
  WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
  WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
  WSAEFAULT:namelen参数不够大。
  WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
  WSAENOTCONN 套接口未连接。
  WSAENOTSOCK:描述字不是一个套接口。
 
触发系统调用
 
int main()
{
    asm volatile(
    "movl $0x34,%eax
	" //使?EAX传递系统调?号52
    "syscall
	" //触发系统调? 
    );
    return 0;
}

 

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

通过如下命令重新启动qemu

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

另开一个终端,设置gdb连接qemu的端口

cd linux-5.4.34/
gdb vmlinux
(gdb) target remote:1234
(gdb) b __x64_sys_getpeername
 技术图片

得到对方的地址

struct sockaddr_in sa;

int len = sizeof(sa);
if(!getpeername(sockfd, (struct sockaddr *)&sa, &len))
{
    printf( "对方IP:%s ", inet_ntoa(sa.sin_addr));
    printf( "对方PORT:%d ", ntohs(sa.sin_port));
}
补充:getsockname和getpeername调度时机很重要,如果调用时机不对,则无法正确获得地址和端口。
TCP
对于服务器来说,在bind以后就可以调用getsockname来获取本地地址和端口,虽然这没有什么太多的意义。getpeername只有在链接建立以后才调用,否则不能正确获得对方地址和端口,所以他的参数描述字一般是链接描述字而非监听套接口描述字。
对于客户端来说,在调用socket时候内核还不会分配IP和端口,此时调用getsockname不会获得正确的端口和地址(当然链接没建立更不可能调用getpeername),当然如果调用了bind 以后可以使用getsockname。想要正确的到对方地址(一般客户端不需要这个功能),则必须在链接建立以后,同样链接建立以后,此时客户端地址和端口就已经被指定,此时是调用getsockname的时机。
UDP
UDP分为链接和没有链接2种(这个到UDP与connect可以找到相关内容)
没有链接的UDP不能调用getpeername,但是可以调用getsockname,和TCP一样,他的地址和端口不是在调用socket就指定了,而是在第一次调用sendto函数以后
已经链接的UDP,在调用connect以后,这2个函数都是可以用的(同样,getpeername也没太大意义。如果你不知道对方的地址和端口,不可能会调用connect)。
 

 五、系统调用总结

 程序执行系统调用大致可归结为以下几个步骤:

1、程序调用libc 库的封装函数。
2、调用软中断int 0x80  进入内核。
3、在内核中首先执行system_call 函数(首先将系统调用号(eax)和可以用到的所有CPU寄存器保存到相应的堆栈中(由SAVE_ALL完成),接着根据系统调用号在系统调用表中查找到对应的系统调用服务例程。
4、执行该服务例程。
5、执行完毕后,转入ret_from_sys_call 例程,从系统调用返回

系统调用由内核分配的一个编号唯一标识(系统调用号)。
所有的系统调用都由一处中枢代码处理,根据调用编号和一个静态表,将调用分派到具体的函数。传递的参数也是由中枢代码处理,这样参数的传递独立于实际的系统调用。从用户态到内核态,以及调用分派和参数传递,都是由汇编语言代码实现的。
为容许用户态和内核态之间的切换,用户进程必须通过一条专用的机器指令,引起处理器/内核对该进程的关注,这需要 C 标准库的协助。内核也必须提供一个例程,来满足切换请求并执行相关操作。该例程不能在用户空间中实现,因为其中需要执行普通应用程序不允许执行的命令。

以上是关于深入理解系统调用的主要内容,如果未能解决你的问题,请参考以下文章

深入理解系统调用

深入理解系统调用

深入理解计算机操作系统(笔记)

深入理解系统调用

深入理解系统调用

深入理解系统调用