sys.stdin.readline是如何通过0x80中断转调到sys_sys.stdin.readline的?

Linux的64位操作系统对32位程序的兼容 - 问题开始
Linux的64位操作系统对32位程序的兼容 - 问题开始
  最近在调试一个关于Open***的程序,由于是远程支持的因此一些很奇怪的现象根本不好找切入点,比如Open***客户端连接服务器正常,虚拟IP地址也已经分配了,tap设备已经打开并没有抱错,然而打开的tap设备不是tap0而是" ",也就是什么都没有,连个空格都不是,这是怎么回事呢?&&&& 为了问题简化,将引起问题的代码从Open***中切出来,得到一个纯粹打开tap设备的代码:int main(int argc, char *argv[]){
  &&& int fd,&&&&& char *clonedev = "/dev/net/tun";&&&&& if( (fd = open(clonedev , O_RDWR)) & 0 ) {&&&&&&&&&&& perror("Opening /dev/net/tun");&&&&&&&&&&&&&&&& }&&&&& memset(&ifr, 0, sizeof(ifr));&&&&& ifr.ifr_flags& |= IFF_TUN;//或者IFF_TAP&&& printf("1:%s\n", ifr.ifr_name);&&&& if( (err = ioctl(fd, TUNSETIFF, &ifr)) & 0 ) {&&&&&&&&&&& perror("ioctl(TUNSETIFF)");&&&&&&&&&&& close(fd);&&&&&&&&&&&&&&&& }&&& printf("2:%s\n", ifr.ifr_name);&&&&&}编译为test执行后,发现第二次打印出"tun0",正常,然后将此程序拷贝给远程的问题机器,却没有打印"tun0"。很多奇怪的问题都和系统相关,于是问到了对方的系统版本,由uname -a得到,发现其实它是一个64位的系统,于是***了一个64位的Red Hat,版本是:2.6.9-78.EL x86_64 GNU/Linux。运行的test是一个在32位系统上编译的程序。由于linux的64位内核对32位程序提供了兼容服务,且x86-64体系也对32位的指令集和寄存器提供了最底层的兼容,想象而言不该出此问题的,在64位系统上检查到了/lib/libc以及/lib/ld-linux等32位的系统库和链接器就更加坚定了“问题不该有”的观念--64位系统兼容32位程序的简单性需要N多层次的支持,机器指令兼容了,操作系统层和编译器就不必再操心指令,操作系统只需要提供系统服务的兼容即可,编译器几乎什么都不需要做,再往上就是系统库了,比如glibc就需要提供两套,为32位程序和64位程序分别提供服务。然而虽然“问题不该有”,事实是问题确实出现了,机器指令是兼容的,操作系统也是兼容的,而系统中确实也有两套libc和ld,那么问题出在哪里呢?&&&& 十有八九是tun的驱动有问题,于是在drivers/net/tun.c的tun_chr_ioctl这个字符设备的ioctl函数中加入dump_stack()调用,编译之,insmod之,然后再次执行test,通过dmesg查看日志,以下是Call Trace:&ffffffffa02c65b9&{:tun:tun_chr_ioctl+0} &ffffffffa02c65dd&{:tun:tun_chr_ioctl+36} &ffffffff&{chrdev_open+952} &ffffffff801a7c86&{sys_ioctl+1006} &ffffffff&{dev_ifsioc+228} &ffffffff801c65a4&{compat_sys_ioctl+379} &ffffffff&{sysenter_do_call+27}其中有一个dev_ifsioc很令人好奇,难道执行流不是通过sys_ioctl直接路由到tun_chr_ioctl的吗?为何还要有一个dev_ifsioc?最后只好看2.6.9内核的代码了。&&&&& 搜索到了以下一行:HANDLE_IOCTL(TUNSETIFF, dev_ifsioc)HANDLE_IOCTL的定义:#define HANDLE_IOCTL(cmd,handler) { (cmd), (ioctl_trans_handler_t)(handler) }, 这明明是想构造一个ioctl_trans数组:struct ioctl_trans {&&&&&& ioctl_trans_handler_&&& struct ioctl_trans *};这个数组提供了内核层次系统调用的64位向32位的兼容性,整个系统所有需要提供兼容性的系统调用都会注册一个ioctl_trans,由此可见dev_ifsioc实际处理了TUNSETIFF这个ioctl命令。64位上的32位程序发起的ioctl系统调用被操作系统路由到了compat_sys_ioctl(具体原因一会儿说):asmlinkage long compat_sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg){&&& ...&&& t = ioctl32_hash_table[ioctl32_hash (cmd)];
  while (t && t-&cmd != cmd)&&&&&&& t = t-&&&& if (t) {&&&&&&& if (t-&handler) { &&&&&&&&&&& lock_kernel();&&&&&&&&&&& error = t-&handler(fd, cmd, arg, filp); //对于TUNSETIFF而言,这里调用dev_ifsioc&&&&&&&&&&& unlock_kernel();&&&&&&&&&&& up_read(&ioctl32_sem);&&&&&&& } else {&&&&&&&&&&& up_read(&ioctl32_sem);&&&&&&&&&&& error = sys_ioctl(fd, cmd, arg);&&&&&&& }&&& } &&& ...}dev_ifsioc的实现如下,它只要提供“兼容性”服务,比如统一64位和32位的数据类型等:static int dev_ifsioc(unsigned int fd, unsigned int cmd, unsigned long arg){&&&&&& struct ifreq32 __user *uifr32;&&& ...&&& mm_segment_t old_&&&&&& &&& uifr32 = compat_ptr(arg); //转换64位的unsigned long数据类型到32位的地址&&& ...&&& switch (cmd) {&&& case SIOCSIFMAP:&&&&&&& ...//不是我们关注的TUNSETIFF&&& default: //对于TUNSETIFF,掉入了default,顺利从uifr32所代表的32位地址处拷贝了ifr结构到内核&&&&&&& if (copy_from_user(&ifr, uifr32, sizeof(*uifr32)))&&&&&&&&&&& return -EFAULT;&&&&&&&&&& }&&& old_fs = get_fs();&&& set_fs (KERNEL_DS);&&& err = sys_ioctl (fd, cmd, (unsigned long)&ifr); //1&&& set_fs (old_fs);&&& if (!err) {&&&&&&& switch (cmd) {& //后面的case明显没有TUNSETIFF&&&&&&& case SIOCGIFFLAGS:&&&&&&& case SIOCGIFMETRIC:&&&&&&& case SIOCGIFMTU:&&&&&&& case SIOCGIFMEM:&&&&&&& case SIOCGIFHWADDR:&&&&&&& case SIOCGIFINDEX:&&&&&&& case SIOCGIFADDR:&&&&&&& case SIOCGIFBRDADDR:&&&&&&& case SIOCGIFDSTADDR:&&&&&&& case SIOCGIFNETMASK:&&&&&&& case SIOCGIFTXQLEN:&&&&&&&&&&& if (copy_to_user(uifr32, &ifr, sizeof(*uifr32)))&&&&&&&&&&&&&&& return -EFAULT;&&&&&&&&&&&&&&&&&& case SIOCGIFMAP:&&&&&&&&&&& ...//不是我们关注的TUNSETIFF&&&&&&& }&&& }&&&}注意“1”处的sys_ioctl调用使用的ifr的地址调用sys_ioctl,而ifr的地址显然只是一个中间变量,它存储在发起系统调用的进程的内核栈上,明显是一个内核栈地址,由此可见,即使sys_ioctl将执行流路由到了tun_chr_ioctl,而tun_chr_ioctl正确地将信息拷贝到了它的参数arg,数据也仅仅填充到了内核栈上,而不是真正的用户进程的地址。如果需要真正将数据拷贝到用户进程空间,我们需要在后面的switch中加一个case,这个case即TUNSETIFF,这样结果就正确了。这明显是一个内核的bug,不知道哪个家伙加了HANDLE_IOCTL(TUNSETIFF, dev_ifsioc)这么一行,却忘记了在dev_ifsioc中处理TUNSETIFF,这几乎可以肯定不是一个人加的,有时间翻一下patchs确认一下。  1.结构体ioctl_trans:struct ioctl_trans {&&&&&& ioctl_trans_handler_&&& struct ioctl_trans *};该结构体提供了一个粘合层,用户可以动态注册一个ioctl_trans以便其提供64位和32位的粘合:extern int register_ioctl32_conversion(unsigned int cmd,&&&&&&&&&&&&&&& ioctl_trans_handler_t handler);extern int unregister_ioctl32_conversion(unsigned int cmd);整个系统的ioctl_trans连接成一个哈希表,放在ioctl32_hash_table变量中。每一个ioctl_trans的handler都是一个回调函数,在其中将64位的数据和32位的数据类型进行统一,统一成64位可以正确识别和处理的,以防在后续的64位代码中出错,比如一个32位的signed int为-1,需要将之转化成64个1而不是32个0加上32个1。2.一套完整的系统调用:如果不这样的话,32位程序的系统调用如何被路由到通过ioctl_trans们进行粘合的代码就成了问题,要知道x86-64已经不使用int 0x80作为触发系统调用的机制了,而使用syscall指令来触发。那么原来的32位程序都是用int 0x80来触发的,这下怎么办?办法就是仍然保留0x80号中断号,将其处理程序设置成ia32_syscall,它在ia32_sys_call_table中找具体的系统调用处理函数,具体在arch/x86_64/ia32/ia32entry.S中:E***Y(ia32_syscall)&&&&&&& CFI_STARTPROC&&&&&&& swapgs&&&&&&& sti&&&&&&& movl %eax,%eax&&&&&&& pushq %rax&&&&&&& cld&&&&&&& S***E_ARGS 0,0,1&&&&&&& GET_THREAD_INFO(%r10)&&&&&&& testl $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),threadinfo_flags(%r10)&&&&&&& jnz ia32_tracesysia32_do_syscall:&&&&&&& cmpl $(IA32_NR_syscalls),%eax&&&&&&& jae& ia32_badsys&&&&&&& IA32_ARG_FIXUP&&&&&&& call *ia32_sys_call_table(,%rax,8) # xxx: rip relative...ia32_sys_call_table:&&&&&&& .quad sys_restart_syscall&&&&&&& .quad sys_exit&&&&&&& .quad stub32_fork&&&&&&& .quad sys_read...&&& .quad compat_sys_ioctl...在arch/x86_64/kernel/traps.c的trap_init函数中将ia32_syscall设置成0x80号中断的处理程序:set_system_gate(IA32_SYSCALL_VECTOR, ia32_syscall);//#define IA32_SYSCALL_VECTOR 0x80那么使用sysenter的怎么办呢? 这是通过在exec的时候由内核检测到其是32位程序是动态将处理代码map到gate处的,要知道x86-64也不使用sysenter机制进行系统调用。那64位的x86-64怎么系统调用呢?在arch/x86-64/kernel/entry.S中有E***Y(system_call)这个标志,在arch/x86_64/kernel/setup64.c中的syscall_init函数中有以下一行:wrmsrl(MSR_LSTAR, system_call);可见64位的x86-64是通过一个MSR寄存器来保存系统调用处理地址的,而不再是通过中断。至于说机器如何处理这个信息以及这个寄存器如何影响系统运行,这已经到x86-64体系的cpu实现硬件问题了,和本文的linux系统的要旨无关,此处简略(再说不简略也不行啊,我也不会啊)。3.总结由于硬件指令的兼容,32位的程序在用户态不受任何影响的运行,由于内核保留了0x80号中断作为32位程序的系统调用服务,因此32位程序可以安全触发0x80号中断使用系统调用,由于内核为0x80中断安排了另一套全新的系统调用表,因此可以安全地转换数据类型成一致的64位类型,再加上应用级别提供了两套c库,可以使64位和32位程序链接不同的库。因此linux的64-32兼容搞得非常好。&&&& 为了看一下在x86-64上64位程序和32位程序是如何执行系统调用的,写一个最简单的测试程序:#include &sys/types.h&#include &unistd.h&int main(){&&&&&&& getpid();}之所以选择getpid是因为它没有参数,最简单,将之在Red Hat 32位机器上按照如下命令行编译:gcc test.c -o test-32 -g然后再将之在64位机器上同样方式编译,只是可执行文件名字变为test-64。接下来首先gdb test-32:(gdb) b main...(gdb) r...(gdb) b getpidBreakpoint 2 at 0xf7f3d430(gdb) disassemble& 0xf7f3d430 0xf7f3d43a0xf7f3d430 &getpid+0&:& mov&&& $0x14,%eax&&& #0x14是20,正是getpid的系统调用号0xf7f3d435 &getpid+5&:& int&&& $0x80&&&&&&&& #32位程序以int 0x80触发系统调用0xf7f3d437 &getpid+7&:& ret&&& 0xf7f3d438 &getpid+8&:& nop&&& 0xf7f3d439 &getpid+9&:& nop&&& End of assembler dump.(gdb)&& 结果全部在,可见即使在64位机器上,32位程序仍然使用int 0x80触发系统调用,在内核中已经注册了0x80的中断处理函数。接下来再试一下64位的程序如何触发系统调用,执行gdb test-64:(gdb) b main...(gdb) r...(gdb) b getpidBreakpoint 2 at 0x32fbf90f40(gdb) disassemble 0x32fbf90f40 0x32fbf90f70Dump of assembler code from 0x32fbf90f40 to 0x32fbf90f70:0xfbf90f40 &getpid+0&:& mov&&& %fs:0x94,%edx0xfbf90f48 &getpid+8&:& test&& %edx,%edx0xfbf90f4a &getpid+10&: mov&&& %edx,%eax0xfbf90f4c &getpid+12&: jle&&& 0x32fbf90f50 &getpid+16&0xfbf90f4e &getpid+14&: repz retq 0xfbf90f50 &getpid+16&: jne&&& 0x32fbf90f5e &getpid+30&0xfbf90f52 &getpid+18&: mov&&& %fs:0x90,%eax0xfbf90f5a &getpid+26&: test&& %eax,%eax0xfbf90f5c &getpid+28&: jne&&& 0x32fbf90f4e &getpid+14&0xfbf90f5e &getpid+30&: mov&&& $0x27,%eax #系统调用号装入eax0xfbf90f63 &getpid+35&: syscall&&&&&&&& #执行系统调用0xfbf90f65 &getpid+37&: test&& %edx,%edx0xfbf90f67 &getpid+39&: jne&&& 0x32fbf90f4e &getpid+14&0xfbf90f69 &getpid+41&: mov&&& %eax,%fs:0x90值得注意的是,在2.6.9内核的x86-64机器上,getpid和32位机器的getpid系统调用号有所不同,在64位上是39号,定义在include/asm-x86_64/unistd.h:#define __NR_getpid&&&&&&&&&&&&&&&&&&&&&&&&&&&& 39__SYSCALL(__NR_getpid, sys_getpid)而刚才看到过,32位兼容的getpid的系统调用号为20,定义在arch/x86_64/ia32/ia32entry.S中:ia32_sys_call_table:...&&& .quad sys_getpid&&&&&&&&&&&&&&& /* 20 */...
  PS:千万不要觉得test.c很简单然后就stepi单指令跟踪哦,因为这会涉及到一大堆跳转,如果你不明白链接的知识,不了解GOT和PIC的话,那就麻烦大了,因此还是直接在getpid处下断比较直观,如果你想顺便把代码重定位和GOT等玩意儿搞了的话,也可以试一下,反正在调试器面前,整个地址空间都会暴露,想看什么都行,当然,要学会让/proc/&pid&/maps等文件帮忙哦。
H3C认证Java认证Oracle认证
基础英语软考英语项目管理英语职场英语
.NETPowerBuilderWeb开发游戏开发Perl
二级模拟试题一级模拟试题一级考试经验四级考试资料
软件测试软件外包系统分析与建模敏捷开发
法律法规历年试题软考英语网络管理员系统架构设计师信息系统监理师
高级通信工程师考试大纲设备环境综合能力
路由技术网络存储无线网络网络设备
CPMP考试prince2认证项目范围管理项目配置管理项目管理案例项目经理项目干系人管理
职称考试题目
招生信息考研政治
网络安全安全设置工具使用手机安全
生物识别传感器物联网传输层物联网前沿技术物联网案例分析
Java核心技术J2ME教程
Linux系统管理Linux编程Linux安全AIX教程
Windows系统管理Windows教程Windows网络管理Windows故障
数据库开发Sybase数据库Informix数据库
&&&&&&&&&&&&&&&
希赛网 版权所有 & &&read操作是任何操作系统里的基本操作,我们来看一下在linux内核里,read文件是怎样实现的。read函数在用户空间是由read系统调用实现的,由编译器编译成软中断int 0x80来进入内核空间,然后在中端门上进入函数sys_read,从而进入内核空间执行read操作。sys_read函数定义在fs/read_write.c文件,定义如下asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count){ struct file */*文件指针*/ ssize_t ret = -EBADF; int fput_ /*轻量级的由文件描述符得到文件指针函数*/ file = fget_light(fd, &fput_needed); if (file) {
/*file结构体里的指示文件读写位置的int变量读取*/
loff_t pos = file_pos_read(file);
/*vfs虚拟文件系统实现read操作的地方*/
ret = vfs_read(file, buf, count, &pos);
/*file结构体里的指示文件读写位置的int变量写入*/
file_pos_write(file, pos);
/*释放file结构体指针*/
fput_light(file, fput_needed); } }首先看看file_pos_read和file_pos_write函数吧,定义如下static inline loff_t file_pos_read(struct file *file){ return file-&f_}static inline void file_pos_write(struct file *file, loff_t pos){ file-&f_pos =}定义很简单,读取的时候就是读出file结构体的f_pos,写入的时候就是写到对应变量。指示文件的读写位置的变量就是在file结构体里。然后看一下fget_light和fput_light函数,定义如下struct file fastcall *fget_light(unsigned int fd, int *fput_needed){ struct file * /*得到当前进程的task_struct的打开的files指针*/ struct files_struct *files = current-& *fput_needed = 0; /*如果只有一个进程使用这个结构体,就不必考虑锁,否则要先得到锁才可以读取*/ if (likely((atomic_read(&files-&count) == 1))) {
/*从files结构体的fd数组上得到file结构体*/
file = fcheck_files(files, fd); } else {
/*先上锁,在得到对应结构体*/
rcu_read_lock();
file = fcheck_files(files, fd);
if (file) {
if (atomic_inc_not_zero(&file-&f_count))
*fput_needed = 1;
/* Didn't get the reference, someone's freed */
file = NULL;
rcu_read_unlock(); } }static inline void fput_light(struct file *file, int fput_needed){ /*释放并减少使用计数*/ if (unlikely(fput_needed))
fput(file);}然后返回来看我们最重要的vfs_read函数,vfs_read函数定义在fs/read_write.c,定义如下ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos){ ssize_ /*首先检查文件是否可以读取,否则返回坏的文件描述符标记*/ if (!(file-&f_mode & FMODE_READ))
return -EBADF; /*如果没有对应的文件操作函数集合,也返回错误*/ if (!file-&f_op || (!file-&f_op-&read && !file-&f_op-&aio_read))
return -EINVAL; /*检查有没有权限*/ if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT; /*检查当前写入的地方有没有被上锁,是否可读写*/ ret = rw_verify_area(READ, file, pos, count); if (ret &= 0) {
/*安全操作*/
ret = security_file_permission (file, MAY_READ);
if (!ret) {
/*如果file结构体里有read函数,就调用*/
if (file-&f_op-&read)
ret = file-&f_op-&read(file, buf, count, pos);
/*否则就调用异步读取的*/
ret = do_sync_read(file, buf, count, pos);
if (ret & 0) {
/*成功读取以后,通知父目录已经读取,并在当前进程结构体上记录*/
fsnotify_access(file-&f_path.dentry);
add_rchar(current, ret);
inc_syscr(current);
} } }然后我们在进入do_sync_read函数看一看异步读取是怎么实现的,do_sync_read函数定义在fs/read_write.c,定义如下ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos){ struct iovec iov = { .iov_base = buf, .iov_len = len };
ssize_ /*初始化读写控制块*/ init_sync_kiocb(&kiocb, filp); kiocb.ki_pos = * kiocb.ki_left = /*调用file_operation结构体的异步读取函数*/ for (;;) {
ret = filp-&f_op-&aio_read(&kiocb, &iov, 1, kiocb.ki_pos);
if (ret != -EIOCBRETRY)
wait_on_retry_sync_kiocb(&kiocb); } /*如果没结束,就等待*/ if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&kiocb); *ppos = kiocb.ki_ }至此,linux内核的read操作就算ok了,linux内核的sys_write和read很相似哦,只要弄明白read,write也一定是可以搞明白的。
最新教程周点击榜
微信扫一扫

参考资料

 

随机推荐