程序8-3 linux/kernel/sys_call.s


  1 /*

  2  *  linux/kernel/system_call.s

  3  *

  4  *  (C) 1991  Linus Torvalds

  5  */

  6

  7 /*

  8  *  system_call.s  contains the system-call low-level handling routines.

  9  * This also contains the timer-interrupt handler, as some of the code is

 10  * the same. The hd- and flopppy-interrupts are also here.

 11  *

 12  * NOTE: This code handles signal-recognition, which happens every time

 13  * after a timer-interrupt and after each system call. Ordinary interrupts

 14  * don't handle signal-recognition, as that would clutter them up totally

 15  * unnecessarily.

 16  *

 17  * Stack layout in 'ret_from_system_call':

 18  *

 19  *       0(%esp) - %eax

 20  *       4(%esp) - %ebx

 21  *       8(%esp) - %ecx

 22  *       C(%esp) - %edx

 23  *      10(%esp) - original %eax        (-1 if not system call)

 24  *      14(%esp) - %fs

 25  *      18(%esp) - %es

 26  *      1C(%esp) - %ds

 27  *      20(%esp) - %eip

 28  *      24(%esp) - %cs

 29  *      28(%esp) - %eflags

 30  *      2C(%esp) - %oldesp

 31  *      30(%esp) - %oldss

 32  */

    /*

     * system_call.s文件包含系统调用(system-call)底层处理子程序。由于有些代码比较类似,

     * 所以同时也包括时钟中断处理(timer-interrupt)句柄。硬盘和软盘的中断处理程序也在这里。

     *

     * 注意:这段代码处理信号(signal)识别,在每次时钟中断和系统调用之后都会进行识别。一般

     * 中断过程并不处理信号识别,因为会给系统造成混乱。

     *

     * 从系统调用返回('ret_from_system_call')时堆栈的内容见上面19-30行。

     */

    # 上面Linus原注释中一般中断过程是指除了系统调用中断(int 0x80)和时钟中断(int 0x20

    # 以外的其他中断。这些中断会在内核态或用户态随机发生,若在这些中断过程中也处理信号识别

    # 的话,就有可能与系统调用中断和时钟中断过程中对信号的识别处理过程相冲突,,违反了内核

    # 代码非抢占原则。因此系统既无必要在这些“其他”中断中处理信号,也不允许这样做。

 33

 34 SIG_CHLD        = 17               # 定义SIG_CHLD信号(子进程停止或结束)。

 35

 36 EAX             = 0x00             # 堆栈中各个寄存器的偏移位置。

 37 EBX             = 0x04

 38 ECX             = 0x08

 39 EDX             = 0x0C

 40 ORIG_EAX        = 0x10             # 如果不是系统调用(是其它中断)时,该值为-1

 41 FS              = 0x14

 42 ES              = 0x18

 43 DS              = 0x1C

 44 EIP             = 0x20             # 44 -- 48行 由CPU自动入栈。

 45 CS              = 0x24

 46 EFLAGS          = 0x28

 47 OLDESP          = 0x2C             # 当特权级变化时,原堆栈指针也会入栈。

 48 OLDSS           = 0x30

 49

    # 以下这些是任务结构(task_struct)中变量的偏移值,参见include/linux/sched.h105行开始。

 50 state   = 0             # these are offsets into the task-struct.  # 进程状态码。

 51 counter = 4             # 任务运行时间计数(递减)(滴答数),运行时间片。

 52 priority = 8            # 运行优先数。任务开始运行时counter=priority,越大则运行时间越长。

 53 signal  = 12            # 是信号位图,每个比特位代表一种信号,信号值=位偏移值+1

 54 sigaction = 16          # MUST be 16 (=len of sigaction) # sigaction结构长度必须是16字节。

 55 blocked = (33*16)       # 受阻塞信号位图的偏移量。

 56

    # 以下定义在sigaction结构中的偏移量,参见include/signal.h,第55行开始。

 57 # offsets within sigaction

 58 sa_handler = 0                     # 信号处理过程的句柄(描述符)。

 59 sa_mask = 4                        # 信号屏蔽码。

 60 sa_flags = 8                       # 信号集。

 61 sa_restorer = 12                   # 恢复函数指针,参见kernel/signal.c程序说明。

 62

 63 nr_system_calls = 82               # Linux 0.12版内核中的系统调用总数。

 64

 65 ENOSYS = 38                        # 系统调用号出错码。

 66

 67 /*

 68  * Ok, I get parallel printer interrupts while using the floppy for some

 69  * strange reason. Urgel. Now I just ignore them.

 70  */

    /*

     * 好了,在使用软驱时我收到了并行打印机中断,很奇怪。呵,现在不管它。

     */

 71 .globl _system_call,_sys_fork,_timer_interrupt,_sys_execve

 72 .globl _hd_interrupt,_floppy_interrupt,_parallel_interrupt

 73 .globl _device_not_available, _coprocessor_error

 74

    # 系统调用号错误时将返回出错码-ENOSYS

 75 .align 2                           # 内存4字节对齐。

 76 bad_sys_call:

 77         pushl $-ENOSYS             # eax中置-ENOSYS

 78         jmp ret_from_sys_call

 

    # 重新执行调度程序入口。调度程序schedule()在(kernel/sched.c119行处开始。

    # 当调度程序schedule()返回时就从ret_from_sys_call处(107行)继续执行。

 79 .align 2

 80 reschedule:

 81         pushl $ret_from_sys_call   # ret_from_sys_call的地址入栈(107行)。

 82         jmp _schedule

 

    #### int 0x80 --linux系统调用入口点(调用中断int 0x80eax中是调用号)。

 83 .align 2

 84 _system_call:

 85         push %ds                   # 保存原段寄存器值。

 86         push %es

 87         push %fs

 88         pushl %eax                 # save the orig_eax   # 保存eax原值。

 

    # 一个系统调用最多可带有3个参数,也可以不带参数。下面入栈的ebxecxedx中放着系统

    # 调用相应C语言函数(见第99行)的调用参数。这几个寄存器入栈的顺序是由GNU gcc规定的,

    # ebx中可存放第1个参数,ecx中存放第2个参数,edx中存放第3个参数。

    # 系统调用语句可参见头文件include/unistd.h中第150200行的系统调用宏。

 89         pushl %edx             

 90         pushl %ecx                 # push %ebx,%ecx,%edx as parameters

 91         pushl %ebx                 # to the system call

 

    # 在保存过段寄存器之后,让ds,es指向内核数据段,而fs指向当前局部数据段,即指向执行本

    # 次系统调用的用户程序的数据段。注意,在Linux 0.12中内核给任务分配的代码和数据内存段

    # 是重叠的,它们的段基址和段限长相同。参见fork.c程序中copy_mem()函数。

 92         movl $0x10,%edx            # set up ds,es to kernel space

 93         mov %dx,%ds

 94         mov %dx,%es

 95         movl $0x17,%edx            # fs points to local data space

 96         mov %dx,%fs

 

 97         cmpl _NR_syscalls,%eax     # 调用号如果超出范围的话就跳转。

 98         jae bad_sys_call

 

    # 下面这句操作数的含义是:调用地址=[_sys_call_table + %eax * 4]。参见程序后的说明。

    # sys_call_table[]是一个指针数组,定义在include/linux/sys.h中,该数组中设置了内核

    # 所有82个系统调用C处理函数的地址。

 99         call _sys_call_table(,%eax,4)   # 间接调用指定功能C函数。

100         pushl %eax                      # 把系统调用返回值入栈。

 

    # 下面101-106行查看当前任务的运行状态。如果不在就绪状态(state不等于0)就去执行调度

    # 程序。如果该任务在就绪状态,但是其时间片已经用完(counter=0),则也去执行调度程序。

    # 例如当后台进程组中的进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程

    # 会收到SIGTTINSIGTTOU信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻

    # 返回。

101 2:

102         movl _current,%eax         # 取当前任务(进程)数据结构指针èeax

103         cmpl $0,state(%eax)        # state

104         jne reschedule

105         cmpl $0,counter(%eax)      # counter

106         je reschedule

 

    # 以下这段代码执行从系统调用C函数返回后,对信号进行识别处理。其他中断服务程序退出时也

    # 将跳转到这里进行处理后才退出中断过程,例如后面131行上的处理器出错中断int 16

    # 首先判别当前任务是否是初始任务task0,如果是则不必对其进行信号量方面的处理,直接返回。

    # 109行上的_task对应C程序中的task[]数组,直接引用task相当于引用task[0]

107 ret_from_sys_call:

108         movl _current,%eax

109         cmpl _task,%eax            # task[0] cannot have signals

110         je 3f                      # 向前(forward)跳转到标号3处退出中断处理。

 

    # 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。

    # 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是

    # 否为用户代码段的选择符 0x000fRPL=3,局部表,代码段)来判断是否为用户任务。如果不是

    # 则说明是某个中断服务程序(例如中断16)跳转到第107行执行到此,于是跳转退出中断程序。

    # 另外,如果原堆栈段选择符不为0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者

    # 不是用户任务,则也退出。

111         cmpw $0x0f,CS(%esp)        # was old code segment supervisor ?

112         jne 3f

113         cmpw $0x17,OLDSS(%esp)     # was stack segment = 0x17 ?

114         jne 3f

 

    # 下面这段代码(115-128)用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32位,

    # 每位代表1种信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值

    # 最小的信号值,再把原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调

    # do_signal()do_signal()在(kernel/signal.c,128)中,其参数包括13个入栈的信息。

    # do_signal()或信号处理函数返回之后,若返回值不为0则再看看是否需要切换进程或继续处理

    # 其它信号。

115         movl signal(%eax),%ebx     # 取信号位图èebx,每1位代表1种信号,共32个信号。

116         movl blocked(%eax),%ecx    # 取阻塞(屏蔽)信号位图èecx

117         notl %ecx                  # 每位取反。

118         andl %ebx,%ecx             # 获得许可的信号位图。

119         bsfl %ecx,%ecx             # 从低位(位0)开始扫描位图,看是否有1的位,

                                       # 若有,则ecx保留该位的偏移值(即第几位0--31)。

120         je 3f                      # 如果没有信号则向前跳转退出。

121         btrl %ecx,%ebx             # 复位该信号(ebx含有原signal位图)。

122         movl %ebx,signal(%eax)     # 重新保存signal位图信息ècurrent->signal

123         incl %ecx                  # 将信号调整为从1开始的数(1--32)。

124         pushl %ecx                 # 信号值入栈作为调用do_signal的参数之一。

125         call _do_signal            # 调用C函数信号处理程序(kernel/signal.c128)。

126         popl %ecx                  # 弹出入栈的信号值。

127         testl %eax, %eax           # 测试返回值,若不为0则跳转到前面标号2101行)处。

128         jne 2b                     # see if we need to switch tasks, or do more signals

 

129 3:      popl %eax                  # eax中含有第100行入栈的系统调用返回值。

130         popl %ebx

131         popl %ecx

132         popl %edx

133         addl $4, %esp              # skip orig_eax    # 跳过(丢弃)原eax值。

134         pop %fs

135         pop %es

136         pop %ds

137         iret

138

    #### int16 -- 处理器错误中断。  类型:错误;无错误码。

    # 这是一个外部的基于硬件的异常。当协处理器检测到自己发生错误时,就会通过ERROR引脚

    # 通知CPU。下面代码用于处理协处理器发出的出错信号。并跳转去执行C函数math_error()

    # kernel/math/error.c 11)。返回后将跳转到标号ret_from_sys_call处继续执行。

139 .align 2

140 _coprocessor_error:

141         push %ds

142         push %es

143         push %fs

144         pushl $-1                   # fill in -1 for orig_eax  # -1,表明不是系统调用。

145         pushl %edx

146         pushl %ecx

147         pushl %ebx

148         pushl %eax

149         movl $0x10,%eax            # ds,es置为指向内核数据段。

150         mov %ax,%ds

151         mov %ax,%es

152         movl $0x17,%eax            # fs置为指向局部数据段(出错程序的数据段)。

153         mov %ax,%fs

154         pushl $ret_from_sys_call   # 把下面调用返回的地址入栈。

155         jmp _math_error            # 执行math_error()kernel/math/error.c11)。

156

    #### int7 -- 设备不存在或协处理器不存在。  类型:错误;无错误码。

    # 如果控制寄存器 CR0 EM(模拟)标志置位,则当CPU 执行一个协处理器指令时就会引发该

    # 中断,这样CPU就可以有机会让这个中断处理程序模拟协处理器指令(181行)。

    # CR0的交换标志TS是在 CPU执行任务转换时设置的。TS 可以用来确定什么时候协处理器中的

    # 内容与CPU 正在执行的任务不匹配了。当CPU 在运行一个协处理器转义指令时发现TS置位时,

    # 就会引发该中断。此时就可以保存前一个任务的协处理器内容,并恢复新任务的协处理器执行

    # 状态(176行)。参见kernel/sched.c92行。该中断最后将转移到标号ret_from_sys_call

    # 处执行下去(检测并处理信号)。

157 .align 2

158 _device_not_available:

159         push %ds

160         push %es

161         push %fs

162         pushl $-1                  # fill in -1 for orig_eax  # -1,表明不是系统调用。

163         pushl %edx

164         pushl %ecx

165         pushl %ebx

166         pushl %eax

167         movl $0x10,%eax            # ds,es置为指向内核数据段。

168         mov %ax,%ds

169         mov %ax,%es

170         movl $0x17,%eax            # fs置为指向局部数据段(出错程序的数据段)。

171         mov %ax,%fs

    # CR0中任务已交换标志TS,并取CR0值。若其中协处理器仿真标志EM没有置位,说明不是

    # EM引起的中断,则恢复任务协处理器状态,执行C函数 math_state_restore(),并在返回时

    # 去执行ret_from_sys_call处的代码。

172         pushl $ret_from_sys_call   # 把下面跳转或调用的返回地址入栈。

173         clts                       # clear TS so that we can use math

174         movl %cr0,%eax

175         testl $0x4,%eax            # EM (math emulation bit)

176         je _math_state_restore     # 执行math_state_restore()kernel/sched.c92行)。

 

    # EM标志置位,则去执行数学仿真程序math_emulate()

177         pushl %ebp

178         pushl %esi

179         pushl %edi

180         pushl $0                   # temporary storage for ORIG_EIP

181         call _math_emulate         # 调用C函数(math/math_emulate.c476行)。

182         addl $4,%esp               # 丢弃临时存储。

183         popl %edi

184         popl %esi

185         popl %ebp

186         ret                        # 这里的ret将跳转到ret_from_sys_call(107)

187

    #### int32 -- (int 0x20) 时钟中断处理程序。中断频率设置为100Hz(include/linux/sched.h,4)

    # 定时芯片8253/8254是在(kernel/sched.c,438)处初始化的。因此这里jiffies10毫秒加1

    # 这段代码将jiffies1,发送结束中断指令给8259控制器,然后用当前特权级作为参数调用

    # C函数do_timer(long CPL)。当调用返回时转去检测并处理信号。

188 .align 2

189 _timer_interrupt:

190         push %ds                   # save ds,es and put kernel data space

191         push %es                   # into them. %fs is used by _system_call

192         push %fs                   # 保存dses并让其指向内核数据段。fs将用于system_call

193         pushl $-1                  # fill in -1 for orig_eax  # -1,表明不是系统调用。

 

    # 下面我们保存寄存器eaxecxedx。这是因为gcc编译器在调用函数时不会保存它们。这里也

    # 保存了ebx寄存器,因为在后面ret_from_sys_call中会用到它。

194         pushl %edx                 # we save %eax,%ecx,%edx as gcc doesn't

195         pushl %ecx                 # save those across function calls. %ebx

196         pushl %ebx                 # is saved as we use that in ret_sys_call

197         pushl %eax

198         movl $0x10,%eax            # ds,es置为指向内核数据段。

199         mov %ax,%ds

200         mov %ax,%es

201         movl $0x17,%eax            # fs置为指向局部数据段(程序的数据段)。

202         mov %ax,%fs

203         incl _jiffies

    # 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。

204         movb $0x20,%al             # EOI to interrupt controller #1

205         outb %al,$0x20

 

    # 下面从堆栈中取出执行系统调用代码的选择符(CS段寄存器值)中的当前特权级别(03)并压入

    # 堆栈,作为do_timer的参数。do_timer()函数执行任务切换、计时等工作,在kernel/sched.c

    # 324行实现。

206         movl CS(%esp),%eax

207         andl $3,%eax               # %eax is CPL (0 or 3, 0=supervisor)

208         pushl %eax

209         call _do_timer             # 'do_timer(long CPL)' does everything from

210         addl $4,%esp               # task switching to accounting ...

211         jmp ret_from_sys_call

212

    #### 这是sys_execve()系统调用。取中断调用程序的代码指针作为参数调用C函数do_execve()

    # do_execve()fs/exec.c207行。

213 .align 2

214 _sys_execve:

215         lea EIP(%esp),%eax         # eax指向堆栈中保存用户程序eip指针处。

216         pushl %eax

217         call _do_execve

218         addl $4,%esp               # 丢弃调用时压入栈的EIP值。

219         ret

220

    #### sys_fork()调用,用于创建子进程,是system_call功能2。原形在include/linux/sys.h中。

    # 首先调用C函数find_empty_process(),取得一个进程号last_pid。若返回负数则说明目前任务

    # 数组已满。然后调用copy_process()复制进程。

221 .align 2

222 _sys_fork:

223         call _find_empty_process   # 为新进程取得进程号last_pid。(kernel/fork.c143)。

224         testl %eax,%eax            # eax中返回进程号。若返回负数则退出。

225         js 1f

226         push %gs

227         pushl %esi

228         pushl %edi

229         pushl %ebp

230         pushl %eax

231         call _copy_process         # 调用C函数copy_process()kernel/fork.c68)。

232         addl $20,%esp              # 丢弃这里所有压栈内容。

233 1:      ret

234

    #### int 46 -- (int 0x2E) 硬盘中断处理程序,响应硬件中断请求IRQ14

    # 当请求的硬盘操作完成或出错就会发出此中断信号。(参见kernel/blk_drv/hd.c)

    # 首先向8259A中断控制从芯片发送结束硬件中断指令(EOI),然后取变量do_hd中的函数指针放入edx

    # 寄存器中,并置do_hdNULL,接着判断edx函数指针是否为空。如果为空,则给edx赋值指向

    # unexpected_hd_interrupt(),用于显示出错信息。随后向8259A主芯片送EOI指令,并调用edx

    # 指针指向的函数: read_intr()write_intr()unexpected_hd_interrupt()

235 _hd_interrupt:

236         pushl %eax

237         pushl %ecx

238         pushl %edx

239         push %ds

240         push %es

241         push %fs

242         movl $0x10,%eax            # ds,es置为内核数据段。

243         mov %ax,%ds

244         mov %ax,%es

245         movl $0x17,%eax            # fs置为调用程序的局部数据段。

246         mov %ax,%fs

    # 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。

247         movb $0x20,%al

248         outb %al,$0xA0             # EOI to interrupt controller #1 # 送从8259A

249         jmp 1f                     # give port chance to breathe    # 这里jmp起延时作用。

250 1:      jmp 1f

    # do_hd定义为一个函数指针,将被赋值read_intr()write_intr()函数地址。放到edx寄存器后

    # 就将do_hd指针变量置为NULL。然后测试得到的函数指针,若该指针为空,则赋予该指针指向C

    # 函数unexpected_hd_interrupt(),以处理未知硬盘中断。

251 1:      xorl %edx,%edx

252         movl %edx,_hd_timeout      # hd_timeout置为0。表示控制器已在规定时间内产生了中断。

253         xchgl _do_hd,%edx

254         testl %edx,%edx

255         jne 1f                     # 若空,则让指针指向C函数unexpected_hd_interrupt()

256         movl $_unexpected_hd_interrupt,%edx

257 1:      outb %al,$0x20             # 8259A主芯片EOI指令(结束硬件中断)。

258         call *%edx                 # "interesting" way of handling intr.

259         pop %fs                    # 上句调用do_hd指向的C函数。

260         pop %es

261         pop %ds

262         popl %edx

263         popl %ecx

264         popl %eax

265         iret

266

    #### int38 -- (int 0x26) 软盘驱动器中断处理程序,响应硬件中断请求IRQ6

    # 其处理过程与上面对硬盘的处理基本一样。(kernel/blk_drv/floppy.c)。

    # 首先向8259A中断控制器主芯片发送EOI指令,然后取变量do_floppy中的函数指针放入eax

    # 寄存器中,并置do_floppyNULL,接着判断eax函数指针是否为空。如为空,则给eax赋值指向

    # unexpected_floppy_interrupt (),用于显示出错信息。随后调用eax指向的函数: rw_interrupt,

    # seek_interrupt,recal_interrupt,reset_interruptunexpected_floppy_interrupt

267 _floppy_interrupt:

268         pushl %eax

269         pushl %ecx

270         pushl %edx

271         push %ds

272         push %es

273         push %fs

274         movl $0x10,%eax            # ds,es置为内核数据段。

275         mov %ax,%ds

276         mov %ax,%es

277         movl $0x17,%eax            # fs置为调用程序的局部数据段。

278         mov %ax,%fs

279         movb $0x20,%al             # 送主8259A中断控制器EOI指令(结束硬件中断)。

280         outb %al,$0x20             # EOI to interrupt controller #1

    # do_floppy为一函数指针,将被赋值实际处理C函数指针。该指针在被交换放到eax寄存器后就将

    # do_floppy变量置空。然后测试eax中原指针是否为空,若是则使指针指向C函数

    # unexpected_floppy_interrupt()

281         xorl %eax,%eax

282         xchgl _do_floppy,%eax

283         testl %eax,%eax            # 测试函数指针是否=NULL?

284         jne 1f                 # 若空,则使指针指向C函数unexpected_floppy_interrupt()

285         movl $_unexpected_floppy_interrupt,%eax

286 1:      call *%eax                 # "interesting" way of handling intr.   # 间接调用。

287         pop %fs                    # 上句调用do_floppy指向的函数。

288         pop %es

289         pop %ds

290         popl %edx

291         popl %ecx

292         popl %eax

293         iret

294

    #### int 39 -- (int 0x27) 并行口中断处理程序,对应硬件中断请求信号IRQ7

    # 本版本内核还未实现。这里只是发送EOI指令。

295 _parallel_interrupt:

296         pushl %eax

297         movb $0x20,%al

298         outb %al,$0x20

299         popl %eax

300         iret