16.20 Linux系统调用 https://scz.617.cn/unix/201205081639.txt D: 2012-05-08 16:39 查看当前Linux内核版本: $ uname -r 2.6.18-4-686 从"www.kernel.org"下载相应内核源码: http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.18.tar.xz 系统调用号在"include/asm-i386/unistd.h"中定义: #define __NR_gettid 224 通常系统调用foo()对应的内核函数是sys_foo(),参看: arch/i386/kernel/syscall_table.S 数组sys_call_table[]用于存放这些内核函数指针。 以前系统调用是通过"int 0x80"进行的。eax对应系统调用号,6个形参依次对应: ebx ecx edx esi edi ebp -------------------------------------------------------------------------- $ gdb /usr/bin/col (gdb) catch syscall open Catchpoint 1 (syscall 'open' [5]) (gdb) r Starting program: /usr/bin/col Catchpoint 1 (call to syscall 'open'), 0xb7ffab54 in ?? () from /lib/ld-linux.so.2 (gdb) x/i $eip-2 0xb7ffab52: int $0x80 (gdb) i r eax ebx ecx edx esi edi ebp eax 0xffffffda -38 ebx 0xbfffd600 -1073752576 ecx 0x0 0 edx 0x0 0 esi 0x1 1 edi 0xb7fe3848 -1208076216 ebp 0xbfffd5e8 0xbfffd5e8 (gdb) x/s $ebx 0xbfffd600: "/usr/local/lib/tls/i686/sse2/cmov/libc.so.6" (gdb) x/5i $eip 0xb7ffab54: pop %ebx 0xb7ffab55: cmp $0xfffff001,%eax 0xb7ffab5a: jae 0xb7ffab5d 0xb7ffab5c: ret 0xb7ffab5d: call 0xb7ffb887 (gdb) ni 0xb7ffab54 in ?? () from /lib/ld-linux.so.2 (gdb) i r eax eax 0xfffffffe -2 -------------------------------------------------------------------------- gdb的"catch syscall ..."断下来时,eip位于"int 0x80"之后,但实际上系统调用 尚未进行,eax里保存的不是返回值。ni之后,eip未变,系统调用已经返回,eax里 保存返回值。就本例而言,由于"/usr/local/lib/tls/i686/sse2/cmov/libc.so.6" 不存在,故eax不是有效值。"man 2 open"得知这个系统调用的第一形参是pathname, 在gdb中查看第一形参。 高版本的Intel芯片提供sysenter/sysexit指令,AMD芯片提供syscall/sysret指令。 后来的Linux系统调用开始使用这些指令,以提高效率。 系统调用gettimeofday()被频繁使用,如果有一个高效实现会降低系统负载。办法之 一是将当前时间写在一个固定位置,其所在内存页被映射到所有进程空间,每次时钟 中断都会自动更新这个固定位置的数据。进程只需要读这个固定位置的数据即可获取 当前时间,不需要跨越用户态、内核态的边界,不需要真实的系统调用。还有一些数 据,比如当前pid,为获取它也不需要真实的系统调用。这些系统调用称为vsyscall。 从Linux 2.5.53开始,有一个固定页,vsyscall page,由内核负责填充有效内容。 在内核初始化阶段调用sysenter_setup(),它会建立一个不可写的页,参看: arch/i386/kernel/sysenter.c C库函数通过跳到"vsyscall page"中某个固定地址进行快速系统调用。该页内容是ELF 格式的,从Linux 2.5.74开始该页被命名为"linux-gate.so.1"。 $ ldd `which col` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/i686/cmov/libc.so.6 (0xb7e60000) /lib/ld-linux.so.2 (0x80000000) 在没有ASLR机制的年代,vsyscall page被映射到固定地址(0xffffe000-0xffffefff)。 从Linux 2.6.18开始,该页被映射到一个随机地址: $ setarch `uname -m` -R cat /proc/self/maps | grep vdso b7fe4000-b7fe5000 r-xp b7fe4000 00:00 0 [vdso] 参看"arch/i386/kernel/vsyscall-sysenter.S",了解vsyscall page的组成。 C库函数调用__kernel_vsyscall()进行系统调用,其位于vsyscall page中。内核通 过ELF Auxiliary Vectors中的AT_SYSINFO将__kernel_vsyscall()的地址传递给用户 态进程。也可以通过GS:0x10获取__kernel_vsyscall()的地址,关于这点,参看: 《在GDB里如何查看GS:0x10的内容》 在当前进程的栈中寻找AT_SYSINFO很不方便,因此libc.so被加载时,会从栈中复制 AT_SYSINFO的值到tcbhead_t.sysinfo,后者可以用GS:0x10访问。 -------------------------------------------------------------------------- /* * gcc-3.3 -Wall -pipe -O3 -s -o vsyscall_demo vsyscall_demo.c */ #include int tid; int main ( int argc, char * argv[], char * envp[] ) { asm \ (" \ movl $224, %eax ;\ call *%gs:0x10 ;\ movl %eax, tid ;\ "); printf( "tid is %d\n", tid ); return( 0 ); } /* end of main */ -------------------------------------------------------------------------- /* * gcc-3.3 -Wall -pipe -O3 -s -o vsyscall_demo vsyscall_demo.c */ #include int main ( int argc, char * argv[], char * envp[] ) { int tid; asm \ ( \ " \ movl $224, %%eax ;\ call *%%gs:0x10 ;\ movl %%eax, %0 ;\ " :"=m"(tid) ); printf( "tid is %d\n", tid ); return( 0 ); } /* end of main */ -------------------------------------------------------------------------- $ gdb ./vsyscall_demo (gdb) catch syscall gettid Catchpoint 1 (syscall 'gettid' [224]) (gdb) r Starting program: /tmp/vsyscall_demo Catchpoint 1 (call to syscall 'gettid'), 0xb7fe4410 in ?? () (gdb) x/i $eip-2 0xb7fe440e: jmp 0xb7fe4403 (gdb) i r eax eax 0xffffffda -38 (gdb) ni 0xb7fe4410 in ?? () (gdb) i r eax eax 0xfec 4076 -------------------------------------------------------------------------- /* * gcc-3.3 -Wall -pipe -O3 -s -o vsyscall_demo vsyscall_demo.c */ #include int main ( int argc, char * argv[], char * envp[] ) { int pid; asm \ ( \ " \ movl $20, %%eax ;\ int $0x80 ;\ movl %%eax, %0 ;\ " :"=m"(pid) ); printf( "pid is %d\n", pid ); return( 0 ); } /* end of main */ -------------------------------------------------------------------------- $ gdb ./vsyscall_demo (gdb) catch syscall getpid Catchpoint 1 (syscall 'getpid' [20]) (gdb) r Starting program: /tmp/vsyscall_demo Catchpoint 1 (call to syscall 'getpid'), 0x0804838d in ?? () (gdb) x/i $eip-2 0x804838b: int $0x80 (gdb) i r eax eax 0xffffffda -38 (gdb) ni 0x0804838d in ?? () (gdb) i r eax eax 0x100c 4108 -------------------------------------------------------------------------- D: 2021-01-13 $ uname -mr 2.6.18-308.24.1.el5 x86_64 $ grep -R "__NR_stat " /usr/include /usr/include/asm-x86_64/unistd.h:#define __NR_stat 4 (gdb) disas _xstat Dump of assembler code for function _xstat: 0x00000037e30c4e30 <_xstat+0>: cmp edi,0x1 0x00000037e30c4e33 <_xstat+3>: mov rax,rsi 0x00000037e30c4e36 <_xstat+6>: ja 0x37e30c4e52 <_xstat+34> 0x00000037e30c4e38 <_xstat+8>: mov rdi,rax 0x00000037e30c4e3b <_xstat+11>: mov rsi,rdx 0x00000037e30c4e3e <_xstat+14>: mov eax,0x4 0x00000037e30c4e43 <_xstat+19>: syscall 0x00000037e30c4e45 <_xstat+21>: cmp rax,0xfffffffffffff000 0x00000037e30c4e4b <_xstat+27>: mov edx,eax 0x00000037e30c4e4d <_xstat+29>: ja 0x37e30c4e68 <_xstat+56> 0x00000037e30c4e4f <_xstat+31>: mov eax,edx 0x00000037e30c4e51 <_xstat+33>: ret 0x00000037e30c4e52 <_xstat+34>: mov rax,QWORD PTR [rip+0x28c14f] # 0x37e3350fa8 0x00000037e30c4e59 <_xstat+41>: mov edx,0xffffffff 0x00000037e30c4e5e <_xstat+46>: mov DWORD PTR fs:[rax],0x16 0x00000037e30c4e65 <_xstat+53>: mov eax,edx 0x00000037e30c4e67 <_xstat+55>: ret 0x00000037e30c4e68 <_xstat+56>: mov rax,QWORD PTR [rip+0x28c139] # 0x37e3350fa8 0x00000037e30c4e6f <_xstat+63>: neg edx 0x00000037e30c4e71 <_xstat+65>: mov DWORD PTR fs:[rax],edx 0x00000037e30c4e74 <_xstat+68>: mov edx,0xffffffff 0x00000037e30c4e79 <_xstat+73>: jmp 0x37e30c4e4f <_xstat+31> End of assembler dump. 系统调用通过syscall进行,rax对应系统调用号,6个形参依次对应: rdi rsi rdx r10 r8 r9 将来rax保存返回值。在x64/Ubuntu上看syscall(2),有讲这些。 "catch syscall stat"会有两次命中,一次是before,一次是after。第一次命中时: (gdb) i r rip rax rdi rsi rdx r10 r8 r9 rip 0x37e30c4e45 0x37e30c4e45 <_xstat+21> rax 0xffffffffffffffda -38 rdi 0x7fffffffd730 140737488344880 rsi 0x7fffffffd6a0 140737488344736 rdx 0x7fffffffd6a0 140737488344736 r10 0x0 0 r8 0xa53 2643 r9 0x12 18 (gdb) x/s $rdi 0x7fffffffd730: "/etc/gentoo-release" (gdb) x/3i $rip-7 0x37e30c4e3e <_xstat+14>: mov eax,0x4 0x37e30c4e43 <_xstat+19>: syscall 0x37e30c4e45 <_xstat+21>: cmp rax,0xfffffffffffff000 rip位于syscall之后,但实际上系统调用尚未进行,rax里保存的不是返回值。ni之 后,rip未变,系统调用已经返回,rax里保存返回值。 (gdb) ni (gdb) i r rip rax rip 0x37e30c4e45 0x37e30c4e45 <_xstat+21> rax 0xfffffffffffffffe -2 没必要ni,直接c就会有第二次命中,rip不变,rax对应返回值。