实验要求

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

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

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

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


实验环境及配置

VMware® Workstation 15 Pro

Ubuntu 16.04.3 LTS

64位操作系统

一、基本理论

1、Linux 的系统调用

当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行 system_call (entry_INT80_32 或 entry_SYSCALL_64)  汇编代码,其中根据系统调用号调用对应的内核处理函数。

具体来说,进入内核后,开始执行对应的中断服务程序 entry_INT80_32 或者 entry_SYSCALL_64。

2、触发系统调用的方法

(1)使用C库函数触发系统调用

以time系统调用为例:

(2)使用 int &0x80 或者 syscall 汇编代码触发系统调用

以time系统调用为例。

32位系统:

64位系统:

二、通过汇编指令触发一个系统调用

1、选择一个系统调用

(1)步骤:

Linux源代码中的 syscall_32.tbl 和 syscall_64.tbl 分别定义了 32位x86 和 64位x86-64的系统调用内核处理函数。

由于我的 Linux 系统是64位的,所以进入Linux源代码中:

~/arch/x86/entry/syscalls/syscall_64.tbl

可以查看系统调用表,如下图所示:

我的学号最后两位为50,所以选择 50号 系统调用。

(2)listen 函数

a. 作用

listen 函数用于监听来自客户端的 tcp socket 的连接请求,一般在调用 bind 函数之后、调用 accept 函数之前调用 listen 函数。

b. 函数原型

#include <sys/socket.h>
int listen(int sockfd, int backlog)

参数 sockfd:被 listen 函数作用的套接字

参数 backlog:侦听队列的长度

返回值:

成功 失败 错误信息
0 -1

EADDRINUSE:另一个socket 也在监听同一个端口

EBADF:参数sockfd为非法的文件描述符。

ENOTSOCK:参数sockfd不是文件描述符。

EOPNOTSUPP:套接字类型不支持listen操作

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

(1)新建服务器端程序:server.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
int main()
{
int sockfd,new_fd,listen_result;
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;
int sin_size;
//建立TCP套接口
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)
{
printf("create socket error");
perror("socket");
exit(1);
}
//初始化结构体,并绑定2323端口
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(2328);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero),8);
//绑定套接口
if(bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))==-1)
{
perror("bind socket error");
exit(1);
}
//创建监听套接口, 监听队列长度为10
//listen_result = listen(sockfd,10);
asm volatile(
"movl $0xa,%%edi\n\t" //listen函数的第二个参数
"movl %1,%%edi\n\t" //listen函数的第一个参数
"movl $0x32,%%eax\n\t" //将系统调用号50存入eax寄存器
"syscall\n\t"
"movq %%rax,%0\n\t"
:"=m"(listen_result)
:"g"(sockfd)
);
if(listen_result == 0)
{
printf("listen is being called\n");
}
if(listen_result ==-1)
{
perror("listen");
exit(1);
} //等待连接
while(1)
{
sin_size = sizeof(struct sockaddr_in); printf("server is run.\n");
//如果建立连接,将产生一个全新的套接字
if((new_fd = accept(sockfd,(struct sockaddr *)&their_addr,&sin_size))==-1)
{
perror("accept");
exit(1);
}
printf("accept success.\n");
//生成一个子进程来完成和客户端的会话,父进程继续监听
if(!fork())
{
printf("create new thred success.\n");
//读取客户端发来的信息
int numbytes;
char buff[256];
memset(buff,0,256);
if((numbytes = recv(new_fd,buff,sizeof(buff),0))==-1)
{
perror("recv");
exit(1);
}
printf("%s",buff);
//将从客户端接收到的信息再发回客户端
if(send(new_fd,buff,strlen(buff),0)==-1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd);
}
close(sockfd);
}

其中对 listen() 函数的调用采用了内嵌汇编指令的形式,即:

        asm volatile(
"movl $0xa,%%edi\n\t" //listen函数的第二个参数
"movl %1,%%edi\n\t" //listen函数的第一个参数
"movl $0x32,%%eax\n\t" //将系统调用号50存入eax寄存器
"syscall\n\t"
"movq %%rax,%0\n\t"
:"=m"(listen_result)
:"g"(sockfd)
);

asm volatile 内联汇编格式

asm volatile(

"Instruction List"

: Output

: Input

: Clobber/Modify

);

a. asm 用来声明一个内联汇编表达式,任何内联汇编表达式都是以它开头,必不可少。

b. volatile 是可选的,如果选用,则向GCC声明不对该内联汇编进行优化。

c. Instruction List 是汇编指令序列,如果有多条指令时:

可以将多条指令放在一队引号中,用 ; 或者 \n 将它们分开;

也可以一条指令放在一对引号中,每条指令一行。

d. Output 用来指定内联汇编语句的输出,相当于系统函数的返回值,格式为:

"=a"(initval)

e. Input 用来指定当前内联汇编语句的输入,相当于系统函数的参数(当该参数为使用C语言的变量的值时,采用这种方法),格式为:

"constraint(variable)"

可以看到,如果使用库函数触发函数调用的话,应该是被注释掉的语句:

        listen_result = listen(sockfd,10);

该函数有两个参数,分别是变量 sockfd 和 常量10,返回值为 listen_result,按照上述规定完成汇编指令触发系统调用。

(2)新建客户端程序:client.c

#include <stdio.h>
#include <stdlib.h> #include <string.h>
#include <netdb.h>
#include <sys/types.h> #include <sys/socket.h> int main(int argc,char *argv[])
{ int sockfd,numbytes;
char buf[100]; struct sockaddr_in their_addr;
//建立一个TCP套接口
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
printf("create socket error.建立一个TCP套接口失败");
exit(1);
}
//初始化结构体,连接到服务器的2323端口
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(2328);
// their_addr.sin_addr = *((struct in_addr *)he->h_addr);
inet_aton( "127.0.0.1", &their_addr.sin_addr ); bzero(&(their_addr.sin_zero),8);
//和服务器建立连接
if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr))==-1)
{
perror("connect");
exit(1);
}
//向服务器发送数据
if(send(sockfd,"hello!socket.",6,0)==-1)
{
perror("send");
exit(1);
}
//接受从服务器返回的信息
if((numbytes = recv(sockfd,buf,100,0))==-1)
{
perror("recv");
exit(1);
}
buf[numbytes] = '/0';
printf("Recive from server:%s",buf);
//关闭socket
close(sockfd); return 0;
}

(3)对两个程序分别编译、链接

a. 代码如下:

gcc -o server server.c -static
gcc -o client client.c -static

格式:gcc -o file file.c

将文件 file.c 编译成可执行文件 file

参数 -static:强制使用静态库链接

参数 -m32:在64位机器上输出32位代码时,需要加上 -32

b. 结果如下:

执行代码前:

可以看出文件夹中目前只有 server.c 和 client.c。

执行代码后:

发现文件夹中已经生成了我们想要的可执行文件 server 和 client。

(4)执行可执行文件

a. 启动 server,表明服务器端启动

代码如下:

sudo  ./server

服务器端启动,结果如下:

可以看到输出 “listen is being called”,表明我们想要调用的系统函数 listen() 已经被成功触发,即系统调用成功。

此时服务器端就等待客户端与其建立链接并通信。

b. 再启动一个终端充当客户端,在该终端中启动 client,表明客户端启动

代码如下:

sudo ./client

客户端启动,结果如下:

可以看到客户端的终端输出 ”Recive from server:hello!0",表明客户端与服务器端已成功建立连接,并且客户端收到了服务器端发回的信息。

c. 此时,服务器端的信息为:

服务器端继续 listen 来自客户端的信息。

如果我们再在另外一个终端内使用 sudo ./client 启动一个客户端,服务器端也会有相应启动成功的信息生成:

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

1、环境配置

(1)安装开发工具

sudo apt install build-essential
sudo apt install qemu # install QEMU
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
sudo apt install axel

以上工具在第一次实验时已经进行了安装。

(2)下载内核源代码

axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
xz -d linux-5.4.34.tar.xz
tar -xvf linux-5.4.34.tar
cd linux-5.4.34

(3)配置内核选项

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)

(4)编译内核

make -j$(nproc) # nproc gives the number of CPU cores/threads available

(5)启动qemu

#测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统最终会kernel panic
qemu-system-x86_64 -kernel arch/x86/boot/bzImage

(6)制作内存根文件系统

a. 下载解压:

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

b. 配置编译、安装:

make menuconfig
#记得要编译成静态链接,不⽤动态链接库。
Settings --->
[*] Build static binary (no shared libs)
#然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中。
make -j$(nproc) && make install

c. 制作内存根文件系统镜像:

在 linux-5.4.34 目录下创建 rootfs 文件夹

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/

d. 准备 init 脚本文件放在根文件系统根目录下(rootfs/init):

新建名为 init 的文档文件,添加如下内容到init文件

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

    给init脚本添加可执行权限

chmod +x init

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

find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz

f. 测试挂在根文件系统,看内核启动完成后是否执行 init 脚本

返回到 linux-5.4.34目录下,启动qemu

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

结果如下:

说明 init 脚本被执行。

2、跟踪调试 Linux 内核

(1)根据第二部分的内容编写利用汇编指令触发系统调用的代码

在 rootfs/home 目录下分别创建两个名为 server.c 和 client.c 的文件,并存入第二部分相应的代码。

(2)使用 gcc 编译成可执行文件 server 和 client

gcc -o server server.c -static
gcc -o client client.c -static

  

(3)重新打包内存根文件系统镜像

find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz

(4)使用 gdb 跟踪调试

方法:

使用 gdb 跟踪调试内核时,在启动 qemu 命令上添加两个参数:

a. -s

作用:

  • 在TCP 1234 端口上创建了一个 gdb-server(如果不想使用1234端口,可以用 -gdb tcp:xxxx 来替代 -s 选项)
  • 打开另外一个窗口,用 gdb 把带符号表的内核镜像 vmlinux 加载进来
  • 然后连接 gdb server,设置断点跟踪内核

b. -S

作用:

  • 表示启动时暂停虚拟机,等待 gdb 执行 continue 指令(可以简写为c)。

步骤:

a. 使用纯命令行启动 qemu

qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"

用该命令启动qemu,可以看到虚拟机一启动就暂停了,终端停留在下面的界面:

  

参数:-nographic -append "console=ttyS0"

启动时不会弹出 qemu 虚拟机窗口,可以在纯命令行下启动虚拟机。

【可以通过 killall qemu-system-x86_64 命令强制关闭虚拟机】

b. 在打开一个终端窗口,进入 linux-5.4.34 目录下,加载内核镜像:

gdb vmlinux

  

c. 连接 gdb server,即在 gdb 中运行下方代码:

(gdb) target remote:1234

  

d. 给文章中使用的系统调用设置断点

方法:

(gdb) b 系统调用函数名

上文可知,我选择的系统调用函数为 listen(),具体信息如下:

代码如下:

(gdb) b __x64_sys_listen

e. 输入 (gdb) c 指令继续运行程序

此时,第一个打开的终端的内容为:

f. 运行编译好的可执行代码 server,使用 gdb 进行单步调试

在第一个终端中输入如下代码:

/home # ls
/home # ./server

此时第二个终端内容为:

在第二个终端中输入:

(gdb) n

结果为: 

 

报错:

GDB 远程调试错误:Remote 'g' packet reply is too long

解决方法:

重新下载 gdb,并修改其中 remote.c 文件内容

由 http://ftp.gnu.org/gnu/gdb/ 下载 gdb的较新版本,此处我下载的是 gdb-7.8.tar.gz,并将其放在了 /home/linux 目录下

进入 /home/linux 目录下,对该文件进行解压缩

tar zxvf gdb-7.8.tar.gz

修改 gdb-7.8/gdb 目录下的 remote.c 文件内容:

将如上图所以的两行原有代码注释掉,然后添加如下的代码:
if (buf_len >  * rsa->sizeof_g_packet) {
rsa->sizeof_g_packet = buf_len ;
for (i = ; i < gdbarch_num_regs (gdbarch); i++)
{
if (rsa->regs->pnum == -)
continue; if (rsa->regs->offset >= rsa->sizeof_g_packet)
rsa->regs->in_g_packet = ;
else
rsa->regs->in_g_packet = ;
}
}

在 gdb-7.8 目录下执行以下命令安装 gdb:

./configure
make
make install

至此,我们再重复上述步骤就可以使用 gdb 对程序设置断点,并且进行单步调试。

(5)使用 gdb 对程序进行单步调试

gdb操作指令:

(gdb) l       查看代码情况
(gdb) n 单步执行
(gdb) step 进入函数内部
(gdb) bt 查看堆栈

重新安装并调整 gdb 之后,按照步骤(4)中的 a - f 依次执行。

a. 当第一个终端运行可执行文件server之后,即:

/home # ./server

第二个终端内容为:

可以看出断点位置。

b. 查看堆栈信息

在第二个终端中输入命令:

(gdb) bt

查看当前堆栈信息,如下所示:

c. 单步调试

在第二个终端输入如下命令,进行单步调试:

(gdb) n

结果如下:

四、分析总结

1、使用 (gdb) bt 查看当前堆栈情况

根据结果显示,函数调用可以分为4层:

顶层: __x64_sys_listen       作用:开放给用户态使用的系统调用函数接口

第二层:do_syscall_64       作用:获取系统调用号,从而调用系统函数

第三层:entry_syscall_64   作用:保存现场工作,调用第二层的 do_syscall_64

第四层:操作系统

2、根据单步调试结果从顶层往下依次查看

(1)断点定位

断点定位为:

/home/linux/linux-5.4.34/net/socket.c 的1688行

执行以下代码,前往相应位置查看:

cd linux/linux-5.4./net
cat -n socket.c

结果为:

进入  __sys_listen(fd, backlog) 函数查看:

int __sys_listen(int fd, int backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn; sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn; err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog); fput_light(sock->file, fput_needed);
}
return err;
}

(2)执行 do_syscall_64 函数

该函数定位在:

/home/linux/linux-5.4.34/arch/x86/entry/common.c 的第300行

 (3)执行 entry_SYSCALL_64 函数

该函数定位在:

/home/linux/linux-5.4.34/arch/x86/entry/entry_64.S 的第184行

3、系统调用总结

(1)用户态的程序代码 server.c 中的内嵌汇编指令 syscall 触发系统调用

(2)通过 MSR 寄存器找到函数入口

中断函数入口为:

/home/linux/linux/-5.4.34/arch/entry/entry_64.S 第145行 ENTRY(entry_SYSCALL_64) 函数,这个函数为 x86_64 系统进行系统调用的通用入口。

ENTRY函数如下:

a. swapgs

使用 swapgs 指令和 下面一系列的压栈动作来保存现场。

b. call do_syscall_64

调用 do_syscall_64 查找系统调用表,获得所要使用的系统调用号。

(3)跳转执行 do_syscall_64

跳转到
/home/linux/linux-5.4.34/x86/entry/common.c 下的 do_system_64函数

a. regs->ax = sys_call_table[nr](regs)

从系统调用表中获得系统调用号,并将其存在到 ax 寄存器中,然后去执行系统调用函数。

b. syscall_return_slowpath(regs)

用于系统调用函数执行结束后,恢复现场

(4)跳转执行系统系统函数 listen

跳转到 /home/linux/linux-5.4.34/net/socket.c 函数,开始执行函数;

(5)恢复现场

函数执行完成后,需要进行现场恢复,因此再次回到:

/home/linux/linux/-5.4.34/arch/x86/entry/entry_64.S

进行现场的恢复。

至此,整个系统调用完成。

参考文章:

https://blog.csdn.net/u013920085/article/details/20574249

https://blog.csdn.net/yangbodong22011/article/details/60399728

https://blog.csdn.net/barry283049/article/details/42970739

Linux操作系统分析 | 深入理解系统调用的更多相关文章

  1. 【Linux操作系统分析】设备驱动处理流程

    1 驱动程序,操作系统,文件系统和应用程序之间的关系 字符设备和块设备映射到操作系统中的文件系统,由文件系统向上提供给应用程序统一的接口用以访问设备. Linux把设备视为文件,称为设备文件,通过对设 ...

  2. Linux操作系统分析__破解操作系统的奥秘

    学号:SA12226343  姓名:sunhongbo 一.操作系统工作的基础 存储程序计算机和堆栈(函数调用堆栈)机制以及中断机制是操作系统工作的基础. 现代计算机仍采用存储程序计算机的结构体系和工 ...

  3. Linux操作系统分析 ------------------中国科技大学

    http://teamtrac.ustcsz.edu.cn/wiki/Linux2014

  4. linux中socket的理解

    对linux中socket的理解 一.socket 一般来说socket有一个别名也叫做套接字. socket起源于Unix,都可以用“打开open –> 读写write/read –> ...

  5. 【转】Linux 概念架构的理解

    转:http://mp.weixin.qq.com/s?__biz=MzA3NDcyMTQyNQ==&mid=400583492&idx=1&sn=3b18c463dcc451 ...

  6. Linux系统的中断、系统调用和调度概述【转】

    转自:http://blog.csdn.net/yanlinwang/article/details/8169725 版权声明:本文为博主原创文章,未经博主允许不得转载. 最近学习Linux操作系统, ...

  7. Linux内核学习笔记1——系统调用原理【转】

    1什么是系统调用 系统调用,顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口.用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文 ...

  8. Linux操作系统主机名(hostname)简介

    http://www.jb51.net/LINUXjishu/10938.html 摘要:本文是关于Linux操作系统主机名(hostname)的文档,对主要配置文件/etc/hosts进行简要的说明 ...

  9. Linux内核分析之扒开系统调用的三层皮(上)

    一.原理总结 本周老师讲的内容主要包括三个方面,用户态.内核态和中断,系统调用概述,以及使用库函数API获取系统当前时间.系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口,也是一种特殊的 ...

随机推荐

  1. 信息竞赛进阶指南--区间最值问题的ST算法

    void ST_prework() { for (int i = 1; i <= n; i++) f[i][0] = a[i]; int t = log(n) / log(2) + 1; for ...

  2. Codeforces 1291 Round #616 (Div. 2) C. Mind Control(超级详细)

    C. Mind Control You and your n−1 friends have found an array of integers a1,a2,-,an. You have decide ...

  3. 图论--2-SAT--POJ Ikki's Story IV - Panda's Trick

    Description liympanda, one of Ikki's friend, likes playing games with Ikki. Today after minesweeping ...

  4. postman(介绍)

    Postman 界面介绍 一. 安装后首次打开 postman,会提示你是否需要登录,登录的话可以云端保存你的收藏及历史记录,不登陆不影响使用.   二. 进入后就是如下图所示的界面了.看到这么多按钮 ...

  5. 使用Jexus 容器化您的 Blazor 应用程序

    在本文中,我们将介绍如何将 Blazor 应用程序放入Jexus 容器以进行开发和部署.我们将使用 .NET Core  CLI,因此无论平台如何,使用的命令都将是相同的. Blazor 托管模型 B ...

  6. hue搭建

    1.安装依赖: sudo yum -y install gcc-c++ asciidoc cyrus-sasl-devel cyrus-sasl-gssapi krb5-devel libxml2-d ...

  7. [NBUT 1224 Happiness Hotel 佩尔方程最小正整数解]连分数法解Pell方程

    题意:求方程x2-Dy2=1的最小正整数解 思路:用连分数法解佩尔方程,关键是找出√d的连分数表示的循环节.具体过程参见:http://m.blog.csdn.net/blog/wh2124335/8 ...

  8. mybatis association的使用

    在上一篇文章中介绍了collection的使用以及java bean,表的结构,今天进行association使用的学习,在多对一的映射关系中,查询到多的一方顺带查询出一的一方是常见的!在此例子中,在 ...

  9. srping mvc RequestMapping实现

    spring mvc中定义请求的url只需要在方法上添加注解: @RequestMapping("aa.mvc")即可定义访问的url地址,但是你是否有考虑过为什么添加这个注解就可 ...

  10. call(),apply(),bind() 区别和用法

    call call 方法第一个参数是要绑定给this的值,后面传入的是一个参数列表.当第一个参数为null.undefined的时候,默认指向window. var arr = [1, 2, 3, 8 ...