本实验讲述的是一个简单的socket网络应用程序,如何一步步的执行到内核。基于这个实验,来大概分析一下,socket网络程序从用户态到内核态的流程。
实验环境是ubuntu18.04,但是用qemu加载linux-5.0.1内核,内核配置是采用x86_64defconfig,并分别制作简易的32位menusOS和64位menuOS,用gdb跟踪内核代码,看看执行过程是怎样的。
我们从上层到下层来看:
一、应用层
menuOS是一个简单的菜单系统,里面加入了简单网络应用,我们主要跟踪网络应用程序。首先来看一下简单网络程序代码:
完整代码在这,可以下载下来看看
https://github.com/mengning/menu.git
本实验用的是lab3代码。
下面来简单看下网络程序代码
int main()
{
BringUpNetInterface();
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi);
MenuConfig("hello", "Hello TCP Client", Hello);
ExecuteMenu();
}
int Replyhi()
{
char szBuf[MAX_BUF_LEN] = "\0";
char szReplyMsg[MAX_BUF_LEN] = "hi\0";
InitializeService();
while (1)
{
ServiceStart();
RecvMsg(szBuf);
SendMsg(szReplyMsg);
ServiceStop();
}
ShutdownService();
return 0;
}
int StartReplyhi(int argc, char *argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr, "Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
Replyhi();
printf("Reply hi TCP Service Started!\n");
}
else
{
/* parent process */
printf("Please input hello...\n");
}
}
int Hello(int argc, char *argv[])
{
char szBuf[MAX_BUF_LEN] = "\0";
char szMsg[MAX_BUF_LEN] = "hello\0";
OpenRemoteService();
SendMsg(szMsg);
RecvMsg(szBuf);
CloseRemoteService();
return 0;
}
#define PrepareSocket(addr,port) \
int sockfd = -1; \
struct sockaddr_in serveraddr; \
struct sockaddr_in clientaddr; \
socklen_t addr_len = sizeof(struct sockaddr); \
serveraddr.sin_family = AF_INET; \
serveraddr.sin_port = htons(port); \
serveraddr.sin_addr.s_addr = inet_addr(addr); \
memset(&serveraddr.sin_zero, 0, 8); \
sockfd = socket(PF_INET,SOCK_STREAM,0);
#define InitServer() \
int ret = bind( sockfd, \
(struct sockaddr *)&serveraddr, \
sizeof(struct sockaddr)); \
if(ret == -1) \
{ \
fprintf(stderr,"Bind Error,%s:%d\n", \
__FILE__,__LINE__); \
close(sockfd); \
return -1; \
} \
listen(sockfd,MAX_CONNECT_QUEUE);
#define InitClient() \
int ret = connect(sockfd, \
(struct sockaddr *)&serveraddr, \
sizeof(struct sockaddr)); \
if(ret == -1) \
{ \
fprintf(stderr,"Connect Error,%s:%d\n", \
__FILE__,__LINE__); \
return -1; \
}
/* public macro */
#define InitializeService() \
PrepareSocket(IP_ADDR,PORT); \
InitServer();
#define ShutdownService() \
close(sockfd);
#define OpenRemoteService() \
PrepareSocket(IP_ADDR,PORT); \
InitClient(); \
int newfd = sockfd;
#define CloseRemoteService() \
close(sockfd);
#define ServiceStart() \
int newfd = accept( sockfd, \
(struct sockaddr *)&clientaddr, \
&addr_len); \
if(newfd == -1) \
{ \
fprintf(stderr,"Accept Error,%s:%d\n", \
__FILE__,__LINE__); \
}
#define ServiceStop() \
close(newfd);
#define RecvMsg(buf) \
ret = recv(newfd,buf,MAX_BUF_LEN,0); \
if(ret > 0) \
{ \
printf("recv \"%s\" from %s:%d\n", \
buf, \
(char*)inet_ntoa(clientaddr.sin_addr), \
ntohs(clientaddr.sin_port)); \
}
#define SendMsg(buf) \
ret = send(newfd,buf,strlen(buf),0); \
if(ret > 0) \
{ \
printf("send \"hi\" to %s:%d\n", \
(char*)inet_ntoa(clientaddr.sin_addr), \
ntohs(clientaddr.sin_port)); \
}
上面其实就是一个简单的TCP网络程序,实现的是一个Hello/hi的功能,当在命令行输入replyhi,hello,会执行相关socketAPI接口,主要就是socket,bind,listen,accept,send,recv,connect,close这几个函数。
应用层写的socke函数,先是调用了libc库中对应的函数,32位libc库中对应函数有一句汇编指令 int $0x80,这是陷入内核的指令,0x80是系统调用的中断号,64位libc库中对应函数的汇编指令是system_call,我们都来跑一下。
先不用qemu加载内核,先在ubuntu上用gdb跟踪一下程序在应用层如何陷入内核。
为了简单,我们不修改Makefile,直接编译,先来编译成32位程序,如下:
gcc -o main *.c -g -m32 -lpthread -static
gdb main
先在ubuntu上调试main函数,跟踪由应用层陷入内核过程。
先在socket加断点,然后打开汇编代码窗口,逐步跟踪汇编,一直输入si,直到出现int $0x80
再来看下本机ubuntu的内核版本
这就说明了32位程序在64位内核版本下陷入内核的指令是int $0x80
下面来看一下64位程序在64位内核版本下陷入内核的指令是什么?
只需要将编译命令改一下
gcc -o main *.c -g -m64 -lpthread -static
gdb main
其他步骤一致,结果如下
64位程序陷入内核的汇编指令是syscall。
二、内核处理过程
接下来跟踪内核,跟踪socket,bind,listen等函数对应的内核处理函数是什么?
同样,分别跟踪32位应用程序和64位程序分别对应的内核处理函数。
首先来跟踪32位应用程序对应的内核处理函数。
make rootfs
cd ..
qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S
再打开一个终端,进入linux-5.0.1目录
gdb
file vmlinux
target remote:1234
查看32位系统调用列表
b sys_socketcall
提示未定义,于是尝试下后面的函数名,看意思是兼容32的socket函数。
__ia32_compat_sys_socketcall
接下来,输入c
过程中提示遇到断点,输入c,或者等到MenuOS启动再加断点也可以。
我们在MenuOS输入replyhi
replyhi
查看net/compat.c源码
找到处理各种socket接口代码,再加个断点,加在859行,然后看看调用了哪些接口
说明首先调用的是__sys_socket函数
接着又调用__sys_bind函数
接着又调用__sys_listen,后面的跟踪方法类似,并且跟应用代码调用顺序保持一致。
下面再来看下,64位应用程序socket API对应的内核处理函数是什么?
查看64位系统调用列表
重新制作rootfs,修改Makefile,生成64位的MenuOS
#
# Makefile for linuxnet/lab3
#
CC_PTHREAD_FLAGS = -lpthread
CC_FLAGS = -c
CC_OUTPUT_FLAGS = -o
CC = gcc
RM = rm
RM_FLAGS = -f
TARGET = main
OBJS = linktable.o menu.o main.o
all: $(OBJS)
$(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS)
rootfs:
gcc -o init linktable.c menu.c main.c -m64 -static -lpthread #将-m32改成-m64
find init | cpio -o -Hnewc |gzip -9 > ../rootfs.img
#qemu-system-x86_64 -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img
.c.o:
$(CC) $(CC_FLAGS) $<
clean:
$(RM) $(RM_FLAGS) $(OBJS) $(TARGET) *.bak init
make rootfs
cd ..
qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S
其他操作基本一致,还在另外一个终端输入同样的命令
file vmlinux
target remote:1234
c
然后 Ctrl+C中断,进入gdb
按照64位系统调用列表,在__x64_sys_socket,__x64_sys_bind,__x64_sys_listen等函数加断点
b __x64_sys_socket
b __x64_sys_bind
b __x64_sys_listen
b __x64_sys_accept
b __x64_sys_sendmsg
b __x64_sys_recvmsg
c
然后在MenuOS里输入replyhi,在gdb调试窗口输入c
但是却没有在__x64_sys_sendmsg和__x64_sys_recvmsg停止,猜测可能内核读写调的不是这两个函数,于是在__x64_sys_read和__x64_sys_write两个函数加断点,再跟踪看看
b __x64_sys_read
b __x64_sys_write
结果在__x64_sys_read和__x64_sys_write停止了,说明recv和send对应的内核函数是__x64_sys_read和__x64_sys_write。
这样对32位和64位应用程序在64位内核上,从应用层陷入内核,以及socket相关API对应的内核处理函数都跟踪完了。
接下来来看一下稍微深层的函数
由代码可知,应用层的socket,bind,listen等函数,在内核中对应的函数是__sys_socket,__sys_bind,__sys_listen等函数,再来看看__sys_socket,__sys_bind,__sys_listen这三个函数的定义,在socket.c文件里定义了
由代码可以看出,在__sys_socket函数中,根据参数初始化了sock结构体,在__sys_bind和__sys_listen等函数中调用了sock->ops->bind函数和sock->op->listen函数。
可以看出这是多态,在__sys_socket函数中根据输入参数初始化了sock结构体,然后在__sys_bind和__sys_listen函数里调用sock->ops->bind和sock->ops->listen函数,这是内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法。
以上就是简述一下socketAPI调用流程的大致过程,都是手敲,如有错误还请指教!