Socket与系统调用深度分析
em ,理论是INT80,但实际调试最后才发现并不是的,所以我的过程中有很多的疑问,到最后我才发现。因此本博文思路并不一定很清晰,但如果你想知道5.0.1的系统调用方式到底是什么,请你继续观看
系统linux-5.0.1 32位
为加快大家查看源码的调用关系 提供 https://elixir.bootlin.com/linux/v5.0.1/source/net/ipv4/tcp_ipv4.c#L202
以下调试都是基于下图的理解进行的,针对图中1,2两个点,博文主要解决四个问题
1 int 0x80中断向量是如何与中断向量表绑定的?
2 socketAPI是如何进入内核调用socket 接口的?
3 socket接口是如何与传输层协议绑定的?
4 sokcet接口是如何调用具体协议的接口的?
Linux 引导过程综述
BIOS->Bootloader->内核初始化:体系结构相关部分-><内核初始化:体系结构无关部分>
内核初始化:体系结构相关部分
1 内核映像结构
2 初始化与保护模式
3 自解压内核
<4 startup_32(head_32.c)>
startup_32(head_32.c)
1 初始化参数(设置段的值,清楚BSS,初始化栈)
2 开启分页机制
3 初始化 Eflags
4 检查处理器类型
5 载入 GDT、IDT
<6 i386_start_kernel>
i386_start_kernel 执行与体系结构无关部分的内核初始化
1 检查中断向量表(IDT)是否已经启动,em,IDT要被初始化第一次
<2 调用start_kernel执行与体系结构无关部分的内核初始化>
start_kernel
使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init())IDT初始化第二次
以上过程只是简单分析从linux启动到终端向量表的初始化的过程,下面我们来看IDT第二次初始化的细节。
中断向量int0x80是如何与中断处理例程绑定?
中断向量表的初始化分有三次:
(1)setup_once: early_idt_handler_array //./arch/x86/kernel/head_32.S line 377
(2)对0~19号的一些和0x80号系统保留中断向量的初始化,在trap_init中完成
(3)对其它中断向量的初始化,在init_IRQ中完成
第二次初始化中断向量表,绑定0x80和SYS_INT80_32
先给出答案:startup_32_smp-->i386_start_kernel -->start_kernel --> trap_init --> idt_setup_traps-->idt_setup_from_table
gdb bt 调试结果如下:
#0 (t=0xc1d99b10 , size=, sys=true,
idt=) at arch/x86/kernel/idt.c:225
#1 0xc1d291d9 in () at arch/x86/kernel/idt.c:267
#2 0xc1d29155 in at arch/x86/kernel/traps.c:934
#3 0xc1d23a5b in () at init/main.c:595
#4 0xc1d2327c in () at arch/x86/kernel/head32.c:56
#5 0xc10001ec in () at arch/x86/kernel/head_32.S:363
#6 0x00000000 in ?? ()
下面我们来具体分析一下。
idt_setup_traps
void __init idt_setup_traps(void)
{
idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}
先不管idt_setup_from_table的作用,先来看看结构体数组def_idts的最后一行SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),SYSG是一个宏,代表系统中断门,作用就是将中断向量IA32_SYSCALL_VECTOR和中断处理例程entry_INT80_32绑定,相信你现在已经明白idt_setup_from_table函数的作用了,就是在填一个table,包括中断向量号及其处理程序。
#define IA32_SYSCALL_VECTOR 0x80 // ./arch/x86/include/asm/irq_vectors.h line 45
def_idts
static const __initconst struct idt_data def_idts[] = {
...
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32) /*int 0x80*/
};
idt_setup_from_table
idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
{
gate_desc desc;
for (; size > 0; t++, size--) {
idt_init_desc(&desc, t);//初始化描述符对应的门:门类型有任务门、中断门、陷阱门和调用门
write_idt_entry(idt, t->vector, &desc);//将对应的门和中断向量写入中断描述符表(IDT)
if (sys)
set_bit(t->vector, system_vectors);//系统设置了一个位图system_vectors,来表示每个中断向量表的使用情况,可以看到,这里是将size个向量表项对应的位图设置为1,表示已经被占用了。
}
}
//1 中断向量表的每个表项叫做一个门描述符(gate descriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。
关于门的描述参考:
https://www.cnblogs.com/qintangtao/p/3325985.html
https://blog.csdn.net/cwcmcw/article/details/21640363
如果你想看write_idt_entry,如下,就是一个简单的内存拷贝。
static inline void native_write_ldt_entry(struct desc_struct *ldt, int entry, const void *desc)
{
memcpy(&ldt[entry], desc, 8);
}
到此我们已经理清楚了中断向量int80绑定了entry_INT80_32,下面我们看第二个问题。
系统调用如何关联系统调用号,系统调用号如何绑定socket接口。
直接查看:linux-5.0.1/arch/x86/entry/syscalls/syscall_32.tbl,以下列出socket相关的系统调用接口,以及对应的系统调用号和内核的socket接口。
102 i386 socketcall sys_socketcall __ia32_compat_sys_socketcall
359 i386 socket sys_socket __ia32_sys_socket
360 i386 socketpair sys_socketpair __ia32_sys_socketpair
361 i386 bind sys_bind __ia32_sys_bind
362 i386 connect sys_connect __ia32_sys_connect
363 i386 listen sys_listen __ia32_sys_listen
364 i386 accept4 sys_accept4 __ia32_sys_accept4
365 i386 getsockopt sys_getsockopt __ia32_compat_sys_getsockopt
366 i386 setsockopt sys_setsockopt __ia32_compat_sys_setsockopt
367 i386 getsockname sys_getsockname __ia32_sys_getsockname
368 i386 getpeername sys_getpeername __ia32_sys_getpeername
369 i386 sendto sys_sendto __ia32_sys_sendto
370 i386 sendmsg sys_sendmsg __ia32_compat_sys_sendmsg
371 i386 recvfrom sys_recvfrom __ia32_compat_sys_recvfrom
372 i386 recvmsg sys_recvmsg __ia32_compat_sys_recvmsg
两种调用方式,一种是以系统调用号102 socketcall 进入sys_socketcall,然后分支进入sys_socket,sys_bind 等,另一种是直接通过自身的系统调用号比如359(socket)找到sys_socket。
我们可以通过调试来查看到底系统是使用哪一种?
用户态调试
先说明一下:由于我的内核无法完成升级,搞了两次暂时没有成功,所以以内核 4.0.5版本的linux系统在用户态调试下调试,如果以后升级成功了再来验证5.0.1.
linux-4.0.5和linux-5.0.1关于socket接口调用的区别:
linux-4.0.5(64) | linux-5.0.1(32) | |
---|---|---|
int80绑定中断向量入口地址 | syscall | entry_INT80_32 |
socketcall对应的系统调用号 | 无 | 102 |
socketd对应的系统调用号 | 41 | 359 |
源代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
return 0;
}
静态编译
gcc -g -o static test.c -static
gdb ./static
用户态系统调用过程:main->socked->mov $0x29 $eax->syscall
mov $0x29 $eax 这个语句相当重要,0x29恰好是十进制的41,对用socket的系统调用编号,然后陷入内核执行系统调用。
第二点说明:如果是linux-5.0.1 我认为上面两条命令应该变为,保存的系统调用编号为102.
mov $0x66 $eax
entry_INT80_32
由于知识水平有限,在用户态我也无法跟踪下去找到int 0x80指令的执行,当然我有想到反汇编static文件,然后查看out.txtx文件的结果,是否有int 0x80指令的调用,但是我没找到!!!是到底是哪儿的问题,是我看不懂汇编吗?不可能,看不懂汇编,我还看不懂 int 0x80 ?????????????好吧,还是我太。。。。菜了。是的,最后我才知道什么原因,请继续看。
objdump -d static > out.txt
用户态看不到,我们去内核态下看吧,注意注意,我的内核态切回5.0.1了。
内核态调试,基于linux-5.0.1
1 在lab3 目录下启动menu终端:
qemu -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s
2 在另一个终端输入
gdb
file vmlinux
break __sys_socket
target remote:1234
c
3 在menu系统中输入replyhi,另一个终端自然进入了内核的断点。
为什么我输入了两次replyhi呢?因为我第一次的断点打在sys_socket处,并没有停下,所以5.0.1系统中的socket系统调用并不是按照第二种方式,而是按照sys_socketcall分发的方式,这儿的细节我已经在上一篇博客最后写过了,现在我们来分析一下:enter_sysenter_32是如何通过系统调用号来找到__sys_socketcall的。你有发现点什么么?为什么进入内核的不是entry_INT80_32 ?
enter_SYSENTER_32中断处理例程到底发生了什么
通过跟踪栈的调用情况,我们来看一看,进入内核之后如何找到__sys_socket的。
(gdb) bt
#0 <__sys_socket >(family=2, type=1, protocol=0) at net/socket.c:1327
#1 0xc1757b98 in __do_sys_socketcall (args=,
call=) at net/socket.c:2555
#2 <__se_sys_socketcall> (call=1, args=-1077721504) at net/socket.c:2527
#3 0xc1002095 in (regs=)
at arch/x86/entry/common.c:334
#4 (regs=0xc7191fb4) at arch/x86/entry/common.c:397
#5 0xc199141b in () at arch/x86/entry/entry_32.S:887
#6 0x00000001 in ?? ()
#7 0xbfc34660 in ?? ()
其中涉及:entry_SYSENTER_32 -> do_fast_syscall_32 -> do_syscall_32_irqs_on -> __do_sys_socketcall-> sys_socket,下面我们一个一个看
entry_SYSENTER_32
ENTRY(entry_SYSENTER_32) 截取重要部分
movl TSS_entry2task_stack(%esp), %esp //保存当进程内核栈
.Lsysenter_past_esp: //保存当前的一些重要寄存器到结构体 pt_regs中
pushl $__USER_DS /* pt_regs->ss */
pushl %ebp /* pt_regs->sp (stashed in bp) */
pushfl /* pt_regs->flags (except IF = 0) */
orl $X86_EFLAGS_IF, (%esp) /* Fix IF */
pushl $__USER_CS /* pt_regs->cs */
pushl $0 /* pt_regs->ip = 0 (placeholder) */
pushl %eax //压栈保存eax!!!还记得我们在用户态下保存的系统调用号吗?5.0.1下应该是102
SAVE_ALL pt_regs_ax=$-ENOSYS /* 保存其他寄存器保在 pt_regs 结构中 */
movl %esp, %eax
call do_fast_syscall_32
在内核启动时,其中会有一个软中断的陷入门,当接收到一个系统调用的时候, 相应的文件就会被调用,然后通过 push 和 SAVE_ALL 将当前用户态的寄存器,保存在 pt_regs 结构中,而结构体pt_regs的内容将在do_syscall_32_irqs_on里面取值。
调试验证,如下图,此时的eax正好是102
do_fast_syscall_32
__visible long do_fast_syscall_32(struct pt_regs *regs)
{
/*
* Called using the internal vDSO SYSENTER/SYSCALL32 calling
* convention. Adjust regs so it looks like we entered using int80.
*/
unsigned long landing_pad = (unsigned long)current->mm->context.vdso +
vdso_image_32.sym_int80_landing_pad;
regs->ip = landing_pad;
enter_from_user_mode();//进入用户态
local_irq_enable();//
if (
#ifdef CONFIG_X86_64
__get_user(*(u32 *)®s->bp,
(u32 __user __force *)(unsigned long)(u32)regs->sp)
#else
get_user(*(u32 *)®s->bp,
(u32 __user __force *)(unsigned long)(u32)regs->sp)
#endif
) {
/* User code screwed up. */
local_irq_disable();
regs->ax = -EFAULT;
prepare_exit_to_usermode(regs);//推出用户态
return 0;
Called using the internal vDSO SYSENTER/SYSCALL32 calling convention. Adjust regs so it looks like we entered using int80.
这句话特别有意思,使其看起来像我们使用了INT80进入内核!!!!!!!!!事实证明它确实不是通过:entry_INT80_32->do_fast_INT80_32->do_syscall_32_irqs_on->__do_sys_socketcall-> sys_socket,有一种挂羊头卖狗肉的感觉,感觉被骗了
实际上linux为了减少系统调用的开销,采取了一种比通过0x80更快的方式,即通过vDSO SYSENTER/SYSCALL32,具体请参考: http://blog.chinaunix.net/uid-27717694-id-4233173.html ,对不起,前面我瞎bb一堆,人家根本就没有通过INT80进入内核。
do_syscall_32_irqs_on
static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
struct thread_info *ti = current_thread_info();
unsigned int nr = (unsigned int)regs->orig_ax; //取系统调用号102
nr = array_index_nospec(nr, IA32_NR_syscalls);//检查nr是否越界
regs->ax = ia32_sys_call_table[nr](regs);//取对调用号对应的函数地址和参数
}
regs的定义
struct pt_regs {
//重点是:orig_ax
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_ax;
/* Return frame for iretq */
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
调试验证,hh了,在编译内核的时候应该禁止编译优化的
虽然我看不到,但是从我们之前的程序来看,regs->orig_ax就是102,从而调用了sys_socketcall.
sys_socketcall
switch (call) {
case SYS_SOCKET:
err = __sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = __sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
case SYS_GETSOCKNAME:
err =
__sys_getsockname(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_GETPEERNAME:
...
通过socket的总接口sys_socketcall进入socket
__sys_socket
int __sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
int flags;
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return retval;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
到此我们的前面两个问题已经解决了。
socket接口是如何与传输层协议绑定的?
由于socket接口对应传输层的协议,包括: TCP,UDP,PING RAW等,那么一个socket接口是如何与这些协议绑定的呢?即linux内核是如何初始化的传输层协议的。
lab3目录下输入调式
qemu -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s -S
另一个终端
gdb
file vmlinux
break inet_init
target remote:1234
c
bt //查看断点的堆栈情况
#0 inet_init () at net/ipv4/af_inet.c:1900
#1 0xc1000a2d in do_one_initcall (fn=0xc1d6cea6 )
at init/main.c:887
#2 0xc1d23da2 in do_initcall_level (level=) at init/main.c:955
#3 do_initcalls () at init/main.c:963
#4 do_basic_setup () at init/main.c:981
#5 kernel_init_freeable () at init/main.c:1136
#6 0xc198af98 in kernel_init (unused=) at init/main.c:1054
#7 0xc1991386 in ret_from_fork () at arch/x86/entry/entry_32.S:722
如上TCP/IP协议的初始化是从Linux内核初始化过程中加载TCP/IP协议栈的,从start_kernel、kernel_init、do_initcalls、inet_init,中间过程过
inet_init
static int __init inet_init(void)
{
struct inet_protosw *q;
struct list_head *r;
int rc = -EINVAL;
sock_skb_cb_check_size(sizeof(struct inet_skb_parm));
rc = proto_register(&tcp_prot, 1); //注册tcp
if (rc)
goto out;
rc = proto_register(&udp_prot, 1);//注册udp
if (rc)
goto out_unregister_tcp_proto;
rc = proto_register(&raw_prot, 1); //注册raw
if (rc)
goto out_unregister_udp_proto;
rc = proto_register(&ping_prot, 1);//注册ping
if (rc)
goto out_unregister_raw_proto;
/*
* Tell SOCKET that we are alive...
*/
(void)sock_register(&inet_family_ops);
}
我们可能到注册不同的协议是通过结构体 struct inet_protosw来实现的,我们来看一下结构体的内容,大部分主要内容都是函数指针对应函数指针,即通过结构体包含协议类型和协议的处理函数对应,将socket接口和不同的协议关联起来了,所以我们写应用程序socket时必须要指明协议族类型比如 tcp对应AF_INET。
struct proto tcp_prot = {
.name = "TCP", //重要 类型
.owner = THIS_MODULE,
.close = tcp_close, //函数close->tcp_close函数
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect, //函数connect->tcp_v4_connect函数
.disconnect = tcp_disconnect,
.accept = inet_csk_accept, //accept
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg, //recvmsg
.sendmsg = tcp_sendmsg, //sendmsg
.sendpage = tcp_sendpage,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
.hash = inet_hash,
.unhash = inet_unhash,
.get_port = inet_csk_get_port,
.enter_memory_pressure = tcp_enter_memory_pressure,
.leave_memory_pressure = tcp_leave_memory_pressure,
.stream_memory_free = tcp_stream_memory_free,
.sockets_allocated = &tcp_sockets_allocated,
.orphan_count = &tcp_orphan_count,
.memory_allocated = &tcp_memory_allocated,
.memory_pressure = &tcp_memory_pressure,
.sysctl_mem = sysctl_tcp_mem,
.sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_tcp_wmem),
.sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_tcp_rmem),
.max_header = MAX_TCP_HEADER,
.obj_size = sizeof(struct tcp_sock),
.slab_flags = SLAB_TYPESAFE_BY_RCU,
.twsk_prot = &tcp_timewait_sock_ops,
.rsk_prot = &tcp_request_sock_ops,
.h.hashinfo = &tcp_hashinfo,
.no_autobind = true,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_tcp_setsockopt,
.compat_getsockopt = compat_tcp_getsockopt,
#endif
.diag_destroy = tcp_abort,
};
好了,到此传输层协议的初始化过程就结束了。
sokcet接口是如何调用具体协议的接口的?
相信看了上面的传输层协议初始化,这儿你就已经清楚了,是通过应用层接口的参数指定了传输层的协议,再通过结构体的绑定,从而调用相应协议的函数接口。
下面只是调试验证一下,由于我们写的协议是AF_INET,所以我的断点就直接打在tcp的tcp_v4_connect函数,看一看是否和我们想象的一样,接着前面我们分析到 __sys_connect,看看它是否是从 __sys_connect ->tcp_v4_connect。
(gdb) bt
#0 (sk=0xc71b06a0, uaddr=0xc7895ec4, addr_len=16)
at net/ipv4/tcp_ipv4.c:203
#1 0xc18151a1 in __inet_stream_connect (sock=0xc77a04e0,
uaddr=, addr_len=, flags=2, is_sendmsg=0)
at net/ipv4/af_inet.c:655
#2 0xc18152c6 in inet_stream_connect (sock=0xc77a04e0, uaddr=0xc7895ec4,
addr_len=16, flags=2) at net/ipv4/af_inet.c:719
#3 0xc1756f44 in <__sys_connect> (fd=,
uservaddr=, addrlen=16) at net/socket.c:1663
#4 0xc1757b78 in __do_sys_socketcall (args=,
call=) at net/socket.c:2561
#5 __se_sys_socketcall (call=3, args=-1076065920) at net/socket.c:2527
#6 0xc1002095 in do_syscall_32_irqs_on (regs=)
at arch/x86/entry/common.c:334
#7 do_fast_syscall_32 (regs=0xc7895fb4) at arch/x86/entry/common.c:397
#8 0xc199141b in entry_SYSENTER_32 () at arch/x86/entry/entry_32.S:887
#9 0x00000003 in ?? ()
#10 0x00000000 in ?? ()
从堆栈情况来看,完全一致,关于tcp_v4_connect分析参考 https://blog.csdn.net/wangpengqi/article/details/9472699
到此终于分析结束了,虽然中间分析有很多小波澜,尤其系统调用方式不是INT80,这个过程让我很矛盾,但总归结束了。
致谢以下博客及文中提到的博客
https://blog.csdn.net/sunnybeike/article/details/6958473
https://blog.csdn.net/yin262/article/details/53928178
https://blog.csdn.net/qyanqing/article/details/8039343
https://cloud.tencent.com/developer/article/1492374