Linux操作系统分析 | 深入理解系统调用
实验要求
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操作系统分析 | 深入理解系统调用的更多相关文章
- 【Linux操作系统分析】设备驱动处理流程
1 驱动程序,操作系统,文件系统和应用程序之间的关系 字符设备和块设备映射到操作系统中的文件系统,由文件系统向上提供给应用程序统一的接口用以访问设备. Linux把设备视为文件,称为设备文件,通过对设 ...
- Linux操作系统分析__破解操作系统的奥秘
学号:SA12226343 姓名:sunhongbo 一.操作系统工作的基础 存储程序计算机和堆栈(函数调用堆栈)机制以及中断机制是操作系统工作的基础. 现代计算机仍采用存储程序计算机的结构体系和工 ...
- Linux操作系统分析 ------------------中国科技大学
http://teamtrac.ustcsz.edu.cn/wiki/Linux2014
- linux中socket的理解
对linux中socket的理解 一.socket 一般来说socket有一个别名也叫做套接字. socket起源于Unix,都可以用“打开open –> 读写write/read –> ...
- 【转】Linux 概念架构的理解
转:http://mp.weixin.qq.com/s?__biz=MzA3NDcyMTQyNQ==&mid=400583492&idx=1&sn=3b18c463dcc451 ...
- Linux系统的中断、系统调用和调度概述【转】
转自:http://blog.csdn.net/yanlinwang/article/details/8169725 版权声明:本文为博主原创文章,未经博主允许不得转载. 最近学习Linux操作系统, ...
- Linux内核学习笔记1——系统调用原理【转】
1什么是系统调用 系统调用,顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口.用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文 ...
- Linux操作系统主机名(hostname)简介
http://www.jb51.net/LINUXjishu/10938.html 摘要:本文是关于Linux操作系统主机名(hostname)的文档,对主要配置文件/etc/hosts进行简要的说明 ...
- Linux内核分析之扒开系统调用的三层皮(上)
一.原理总结 本周老师讲的内容主要包括三个方面,用户态.内核态和中断,系统调用概述,以及使用库函数API获取系统当前时间.系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口,也是一种特殊的 ...
随机推荐
- Codeforces Round #623 (Div. 2, based on VK Cup 2019-2020 - Elimination Round, Engine) B. Homecoming
After a long party Petya decided to return home, but he turned out to be at the opposite end of the ...
- ELK+kafka日志收集分析系统
环境: 服务器IP 软件 版本 192.168.0.156 zookeeper+kafka zk:3.4.14 kafka:2.11-2.2.0 192.168.0.42 zookeeper+kaf ...
- java基础篇 之 foreach探索
我们看下这段代码: public class Main { public static void main(String[] args) { List list = new ArrayList(); ...
- 【Hadoop离线基础总结】Apache Hadoop的三种运行环境介绍及standAlone环境搭建
Apache Hadoop的三种运行环境介绍及standAlone环境搭建 三种运行环境 standAlone环境 单机版的hadoop运行环境 伪分布式环境 主节点都在一台机器上,从节点分开到其他机 ...
- 我去,你竟然还不会用 synchronized
二哥,离你上一篇我去已经过去两周时间了,这个系列还不打算更新吗?着急着看呢. 以上是读者 Jason 发来的一条信息,不看不知道,一看真的是吓一跳,上次我去是 4 月 3 号更新的,离现在一个多月了, ...
- 关于Fragment的点击切换数据滞留问题
场景再现:当我使用tabLayout + Fragment 切换不同的fragment时,出现了数据重复显示的问题: 思考逻辑: - 每次切换fragment都会重新获取数据,但是list集合是全局的 ...
- 转载-git使用之忽略不需要上传的文件的几种方式
在我们使用git 的时候通常会遇到一些问题,一些文件我创建了但是我并不想上传或者有些文件我修改了但是并不想上传(为了适应个自己的开发环境),但是在每次git status的时候总能看到它,不仅感到很心 ...
- vue-cli中浏览器图标的配置
在VUE全家桶项目里面,这里给大家提供了2种方案,进行浏览器图标的配置. a):先把图片准备好,放在static文件夹下,再找到根目录下的index.html文件,并打开,在HTML文档的<he ...
- [hdu5200]离线+标记
思路:按顺序处理,新建一堆然后向左右合并,不过巧妙地用了标记数组来记录和统计答案. #pragma comment(linker, "/STACK:10240000,10240000&quo ...
- Redis 6.0 多线程重磅发布!!!
Redis 6.0在5.2号这个美好的日子里悄无声息的发布了,这次发布在IT圈犹如一颗惊雷一般,因为这是redis最大的一次改版,首次加入了多线程. 作者Antirez在RC1版本发布时在他的博客写下 ...