系统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.
源代码
#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 -m32
gdb ./static
//反汇编 main 和 socket
disassemble main
disassemble 0x806e790
用户态系统调用过程:main->socked->mov $0x66 $eax->call *%gs:0x10
库函数socket将eax寄存器设置为socketcall系统调用的调用号0x66,然后调用%gs:0x10所指向的函数。在gdb中,无法查看非DS段的数据内容,所以无法查看%gs:0x10所保存的实际数值,但它对应一个函数地址,而这个地址就是内核为我们映射的系统调用入口代码,这个函数地址里面应该包含了int 0x80。
mov $0x66 $eax 这个语句相当重要,0x66恰好是十进制的102,保存到了eax寄存器里面了,对用socketcall的系统调用编号,然后陷入内核执行系统调用。
由于知识水平有限,在用户态我也无法跟踪下去找到int 0x80指令的执行,当然我有想到反汇编static文件,然后查看out.txtx文件的结果,是否有int 0x80指令的调用
objdump -d static > out.txt
确实有,在如下几个文件里面都找到了int 0x80的踪影:
__libc_setup_tls>:
_exit
_tunables_init
_dl_sysinfo_int80
_restore_rt
_restore
__brk
看以上几个函数名,我猜是_dl_sysinfo_int80 调用INT80指令进入内核态的,但我又没有证据,所以用户态下只能暂时告一段落。
用户态看不到,我们去内核态下看吧,注意注意,我的内核态切回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分发的方式,这儿的细节我已经在上一篇博客最后写过了。
堆栈情况如下:
(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 ?? ()
现在我们来分析一下:enter_sysenter_32是如何通过系统调用号来找到__sys_socketcall的。你有发现点什么么?为什么进入内核的不是entry_INT80_32 ? int 0x80不是对应它吗?为什么是enter_SYSENTER_32,先不管我们就按照enter_SYSENTER_32分析下去。
enter_SYSENTER_32中断处理例程到底发生了什么
其中涉及: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->entry_INT80_32更快的方式,仿照entry_INT80_32,即通过vDSO SYSENTER/SYSCALL32,具体请参考: http://blog.chinaunix.net/uid-27717694-id-4233173.html ,但是二者确实很类似: 可以通过网站 https://elixir.bootlin.com/linux/v5.0.1/source/net/ipv4/tcp_ipv4.c#L202 搜索entry_INT80_32的实现,和entry_SYSENTER_32是差不多的。
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,这个过程让我很矛盾,但总归结束了。