标签:download swap share 内存 虚拟机 加载 vol 代码 ted
找一个系统调用,系统调用号为学号最后2位相同的系统调用
通过汇编指令触发该系统调用
通过gdb跟踪该系统调用的内核处理过程
重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
本人学号为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
。
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体系结构下普通的函数调用和系统调用都是通过寄存器传递参数,RDI
、RSI
、RDX
、RCX
、R8
、R9
这6个寄存器用作函数/系统调用参数传递,依次对应第 1 参数到第 6 个参数,并且使用EAX
传递系统调用号。对应上述汇编代码可以清楚地看到往RDI
、RSI
、RDX
、EAX
这四个寄存器中存放数据,往EAX
寄存器中存放系统调用号$0x2a
。(其中RDX
、RDI
只使用了低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
也在调用connect
、send
、write
时是一个待传入的参数,但问题是在这段嵌入式汇编的前后,s
的值发生了变化,导致s
不能正常指向socket
,所以出现了无法发送与接收数据的情况。解决方案是将s
的值不存到寄存器,而使用:"m"(s),"g"(&addr),"g"(sizeof(addr))
来指向待输入的参数。
虽然bug解决了,但是其中的原理我还是没有搞懂,为什么将s的值存到寄存器中,会导致s值的变化,并且我也尝试了指定特定的寄存器去保存s,问题还是存在。
配置内核选项(下载内核见上篇):
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
加载系统成功如下:
首先将通过命令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
的系统调用不受影响,所以实验仍然可以继续进行。
当上述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
执行:
函数具体的代码在文章一开始就列出,这里不再详述。
综上,系统调用的过程可以总结如下:
syscall
指令
系统调用处理入口entry_SYSCALL_64
保存现场
do_syscall_64
sys_call_table
数组syscall_return_slowpath
恢复现场
系统调用返回sysret
继续执行syscall
指令的下一条指令
此外,需要注意的一点是,上述过程,在保存现场与系统调用返回之间,属于内核态,之外属于用户态。
标签:download swap share 内存 虚拟机 加载 vol 代码 ted
原文地址:https://www.cnblogs.com/winkkk/p/linux_2020_lab2.html