⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 5.txt

📁 Linux内核源代码分析/电子书
💻 TXT
📖 第 1 页 / 共 2 页
字号:
182:接下来的代码仍然是system_call的一部分,它是一个也可以命名为ret_from_sys_call和ret_from_intr的独立入口点。它们偶尔会被C直接调用,也可以从system_call的其他部分跳转过来。185:接下来的几行检测“下半部分(bottom half)”是否激活;如果激活了,就跳转到handle_bottom_half标号(242行)并立即开始处理。下半部分是中断进程的一部分,将在下一章中讨论。189:检查该进程是否为再次调度做了标记(记住表达式$0就是常量0的系统简单表示)。如果的确如此,就跳转到reschedule标号(247行)。191:检测是否还有挂起的信号,如果有的话,下一行就向前跳转到signal_return(197行)。193:restore_all标号是system_call的退出点。其主体就是简单的RESTORE_ALL宏(100行),该宏将恢复早先由SAVE_ALL存储的参数并返回给system_call的调用者。197:当system_call从系统调用返回前,如果它检测到需要将信号传送给当前的进程时,才会执行到signal_return。它通过使中断再次可用开始执行,有关内容将在第6章中介绍。199:如果返回虚拟8086模式(这不是本书的主题),就向前跳转到v86_signal_return(207行)。202:system_call要调用C函数do_signal(3364行,在第6章中讨论)来释放信号。do_signal需要两个参数,这两个参数都是通过寄存器传递的;第一个是EAX寄存器,另一个是EDX寄存器。system_call(在200行)早已把第一个参数的值赋给了EAX;现在,就把EDX寄存器和寄存器本身进行XOR操作,从而将其清0,这样do_signal就认为这是一个空指针。203:调用do_signal传递信号,并且跳回到restore_all(193行)结束。207:由于虚拟8086模式不是本书的主题,我们将忽略大部分v86_signal_return。然而,它和signal_return的情况非常类似。215:如果当前进程的系统调用正由其祖先跟踪,就像strace程序中那样,那么就可以执行到tracesys标号。这一部分的基本思想如同179行一样是通过syscall_table调用系统函数,但是这里把该调用和对syscall_trace函数的调用捆绑在一起。后面的这个函数在本书中并没有涉及到,它能够终止当前进程并通知其祖先注意当前进程将要激活一个系统调用。EAX操作和这些代码的交错使用最初可能容易令人产生困惑。system_call把存储在堆栈中的EAX拷贝赋给-ENOSYS,调用syscall_trace,在172行再从所做的拷贝中恢复EAX的值,调用实际的系统调用,把系统调用的返回值置入堆栈中EAX的位置,再次调用syscall_trace。这种方式背后的原因是syscall_trace(或者更准确地说是它所要用到的跟踪程序)需要知道它是在实际系统调用之前还是之后被调用的。-ENOSYS的值能够用来指示它是在实际系统调用执行之前被调用的,因为实际中所有实现的系统调用的执行都不会返回-ENOSYS。因此,EAX在堆栈中的备份在第一次调用syscall_trace之前是-ENOSYS,但是在第二次调用syscall_trace之前就不再是了(除非是调用sys_ni_syscall的时候,在这种情况下,我们并不关心是怎样跟踪的)。218行和219行中EAX的作用只是找出要调用的系统调用,这和无须跟踪的情况是一致的。222:被跟踪的系统调用已经返回,流程控制跳转回ret_from_sys_call(184行)并以与普通的无须跟踪的情况相同的方式结束。223:当系统调用的数目越界时,就可以执行到badsys标号。在这种情况下,system_call必须返回-ENOSYS(ENOSYS在82行将它赋值为38)。正如前面提到的一样,调用者会识别出这是一个错误,因为返回值在-1到-4 095之间。228:在诸如除零错误(请参见279行)之类的CPU异常中断情况下将执行到ret_from_exception标号;但是system_call内部的所有代码都不会执行到这个标号。如果有下半部分是激活的,现在就是它在起作用了。233:处理完下半部分之后或者从上面的情况简单执行下来(虽然没有下半部分是激活的,但是同样也触发了CPU异常),就执行到了ret_from_intr标号。这是一个全局可访问符号,因此可能在内核的其他部分也会有对它的调用。237:被保存的CPU的EFLAGS和CS寄存器在此已经被并入EAX,因而高24位的值(其中恰好包含了一位在70行定义的非常有用的VM_MASK)来源于EFLAGS,其他低8位的值来源于CS。该行隐式的同时对这两部分进行测试以判断进程是返回虚拟8086模式(这是VM_MASK的部分)还是用户模式(这是3的部分—用户模式的优先等级是3)。下面是近似的等价C代码:238:如果这些条件中有一个能得到满足,流程控制就跳转到ret_with_reschedule(188行)标号,测试在system_call返回之前进程是否需要再次调度。否则,调用者就是一个内核任务,因此system_call通过跳转到restore_all (193行)来跳过重新调度的内容。242:无论何时system_call使用一个下半部分服务时都可以执行到handle_bottom_half标号。它简单地调用第6章中介绍的C函数bottom_half(29126行),然后跳回到ret_from_intr(233行)。248:system_call的最后一个部分在reschedule标号之下。当产生系统调用的进程已经被标记为需要进行重新调度时,就可以执行到这个标号;一般说来,这是因为进程的时间片已经用完了—也就是说,进程到目前为止已经尽可能地拥有CPU了,应该给其他进程一个机会来运行了。因此,在必要的情况下就可以调用C函数schedule(26686行)交出CPU,同时流程控制转回249行。CPU调度是第7章中讨论的一个主题。5.2.2   lcall7Linux支持Intel二进制兼容规范标准的版本2(iBCS2)(iBCS2中的小写字母i显然是有意的,但是该标准却没有对此进行解释,这样看来似乎和现实的Intel系列的CPU例如i386、i486等等是一致的)。iBCS2的规范中规定了所有基于x86的Unix系统的应用程序的标准内核接口,这些系统不仅包括Linux,而且还包括其他自由的x86 Unix(例如FreeBSD),也还包括Solaris/x86、SCO Unix等等。这些标准接口使得为其他Unix系统开发的二进制商业软件在Linux系统中能够直接运行,反之亦然(而且,近期新开发软件向其他Unix移植的情况越来越多)。例如,Corel公司的WordPerfect的SCO Unix的二进制代码在还没有Linux的本地版本的WordPerfect之前就可以使用iBCS2在Linux上良好地运行。iBCS2标准有很多组成部分,但是我们现在关心的是这些系统调用如何协调一致来适应这些迥然不同的Unix系统。这是通过lcall7调用门实现的。它是一个相当简单的汇编函数(尤其是和system_call相比而言更是如此),仅仅定位并全权委托一个C函数来处理细节。(调用门是x86 CPU的一种特性,通过这种特性用户任务可以在安全受控的模式下调用内核代码。)这种调用门在6802行进行设定。 lcall7135:前面的几行将通过调整处理器堆栈以使堆栈的内容和system_call预期的相同—system_call中的一些代码将会完成清理工作,这样所有的内容都可以连续存放了。145:基于同样的思想,lcall7把指向当前任务的指针置入EBX寄存器,这一点和system_call的情况是相同的。但是,它的执行方式却与system_call不同,这就比较奇怪了。这三行可以等价地按如下形式书写:这种实现的执行速度并不比原有的更快,在将宏展开以后,实际上这还是同样的三条指令以不同的次序组合在一起而已。这样做的优点是可以和文件中的其他代码更为一致,而且代码也许会更清晰一些。148:取得指向当前任务exec_domain域的指针,使用这个域以获取指向其lcall7处理程序的指针,接着调用这个处理程序。本书中并没有对执行域(execution domains)进行详细说明—但是简单说来,内核使用执行域实现了部分iBCS2标准。在15977行你可以找到struct exec_domain结构。default_exec_domain(22807行)是缺省的执行域,它拥有一个缺省的lcall7处理程序。它就是no_lcall7(22820行)。其基本的执行方式类似于SVR4风格的Unix,如果调用进程没有成功,就传送一个分段违例信号(segmentation violation signal)给调用的进程。152:跳转到ret_from_sys_call标号(184行—注意这是在system_call内部的)清除并返回,就像是正常的系统调用一样。5.3   系统调用样例现在你已经知道了系统调用是如何激活的,接下来我们将通过对几个系统调用例子的剖析来了解一下它们的工作方式。注意系统调用foo几乎都是使用名为sys_foo的内核函数实现的,但是在某些情况下该函数也会使用一个名为do_foo的辅助函数。1. sys_ni_syscall29185:sys_ni_syscall的确是最简单的系统调用;它只是简单地返回ENOSYS错误。最初的时候这可能显得没有什么作用,但是它的确是有用的。实际上,sys_ni_syscall在sys_call_table中占据了很多位置—而且其原因并不只有一个。开始的时候,sys_ni_syscall在位置0(374行),因为,如果漏洞百出的代码错误地调用了system_call—例如,没有初始化作为参数传递给system_call的变量,在这种偶然的变量定义中,0是最可能的值。如果我们能够避免这种情况,那么在错误发生时就不用采取像杀掉进程这样的措施(当然,只要允许有用工作的进行,就不可能防止所有的错误)。这种使用表的元素0作为抵御错误的手段在内核中作为良好的经验而被广泛使用。而且,你还会发现sys_ni_syscall在表中明显出现的地方就多达十几处。这些条目代表了那些已经从内核中移出的系统调用—例如在418行,就代替了已经废弃了的prof系统调用。我们不能简单地把另外的实际系统调用放在这里,因为老的二进制代码可能还会使用到这些已经废弃了的系统调用号。如果一个程序试图调用这些老的系统调用,但是结果却与预期的完全不同,例如打开了一个文件,这会令人感到奇怪的。最后,sys_ni_syscall将占据表尾部所有未用的空间;这一点是在从572行到574行的代码中实现的,它根据需要重复使用这些项来填充表。由于sys_ni_syscall只是简单返回ENOSYS错误号,对它的调用和跳转到system_call中的badsys标号作用是相同的—也就是说,使用指向这些表项的系统调用号和在表外对整个表进行全部索引具有相同的作用。因此,我们不用改变NR_syscalls就可以在表中增加(或者删除)系统调用,但是其效果与我们真的对NR_syscalls进行了修改一样(不管怎样,这都是由NR_syscalls所建立的限制条件所决定的)。到现在也许你已经猜到了,sys_ni_syscall中的“ni”并不是指Monty Python的“说 ‘Ni’ 的骑士”;而是指“not implemented(没有实现)”。对于这个简单的函数我们需要研究的另外一个问题是asmlinkage标志。这是为一些gcc功能定义的一个宏,它告诉编译器该函数不希望从寄存器中(这是一种普通的优化 )取得任何参数,而希望仅仅从CPU堆栈中取得参数。回忆一下我们前面提到过system_call使用第一个参数作为系统调用的数目,同时还允许另外四个参数和系统调用一起传递。system_call通过把其他参数(这些参数是通过寄存器传递过来的)滞留在堆栈中简单地实现了这种技巧。所有的系统调用都使用asmlinkage标志作了标记,因此它们都要查找堆栈以获得参数。当然,在sys_ni_syscall的情况下这并没有任何区别,因为sys_ni_syscall并不需要任何参数。但是对于其他大部分系统调用来说,这就是个问题了。并且,由于在其他很多函数前面都有asmlinkage标志,我想你也应该对它有些了解。2. sys_time31394:sys_time是包含几个重要概念的简单系统调用。它实现了系统调用time,返回值是从某个特定的时间点(1970年1月1日午夜UTC)以来经过的秒数。这个数字被作为全局变量xtime(请参见26095行;它被声明为volatile型的变量,因为它可以通过中断加以修改,这一点我们在第6章中就会看到)的一部分,通过CURRENT_TIME宏(请参见16598行)可以访问它。31400:该函数非常直接地实现了它的简单定义。当前时间首先被存储在局部变量i中。31402:如果所提供的指针tloc是非空的,返回值也将被拷贝到指针指向的位置。该函数的一个微妙之处就在于此;它把i拷贝到用户空间中,而不是使用CURRENT_ TIME宏来重新对其进行计算,这基于两个原因:

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -