|
|
其实真正调用的还是系统函数syscall(SYS_read),也就是sys_read()函数中,在Linux2.6.37中的利用几个宏定义实现。
Linux 系统调用(SCI,system call interface)的实现机制实际上是一个多路汇聚以及分解的过程,该汇聚点就是 0x80 中断这个入口点(X86 系统结构)。也就是说,所有系统调用都从用户空间中汇聚到 0x80 中断点,同时保存具体的系统调用号。当 0x80 中断处理程序运行时,将根据系统调用号对不同的系统调用分别处理(调用不同的内核函数处理)。
引起系统调用的两种途径
(1)int $0×80 , 老式linux内核版本中引起系统调用的唯一方式
(2)sysenter汇编指令
在Linux内核中使用下面的宏进行系统调用|
|
其中SYSCALL_DEFINE3的宏定义如下:
|
|
##的意思就是宏中的字符直接替换,
如果name = read,那么在宏中__NR_##name就替换成了__NR_read了。 __NR_##name是系统调用号,##指的是两次宏展开.即用实际的系统调用名字代替"name",然后再把__NR_...展开.如name == ioctl,则为__NR_ioctl。
|
|
不管是否定义CONFIG_FTRACE_SYSCALLS宏,最终都会执行 下面的这个宏定义:
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
|
|
最终会调用下面类型的宏定义:
asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))
也就是我们前面提到的sys_read()系统函数。
asmlinkage通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词!这和我们上一篇文章quagga中提到的宏定义,有异曲同工之妙。
也就是宏定义中的下面代码:
|
|
代码解析:
- fget_light() :根据 fd 指定的索引,从当前进程描述符中取出相应的 file 对象(见图3)。
- 如果没找到指定的 file 对象,则返回错误
- 如果找到了指定的 file 对象:
- 调用 file_pos_read() 函数取出此次读写文件的当前位置。
- 调用 vfs_read() 执行文件读取操作,而这个函数最终调用 file->f_op.read() 指向的函数,代码如下:
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
- 调用 file_pos_write() 更新文件的当前读写位置。
- 调用 fput_light() 更新文件的引用计数。
- 最后返回读取数据的字节数。
到此,虚拟文件系统层所做的处理就完成了,控制权交给了 ext2 文件系统层。
http://blogold.chinaunix.net/u3/104447/showart_2527011.html
使用 syscall/sysret 指令
在这里,我想介绍一下 syscall 与 sysret 这对指令。关于这对指令请以 AMD 的手册为准,毕竟它们的 AMD 的产物。当然不能说 Intel 的不对,Intel 对 syscall/sysret 的完全兼容的。
可是,在 AMD 与 Intel 的 processor 上还是有区别的:
- 在 AMD 的 processor 上:syscall/sysret 指令在 long mode 和 protected mode(指的是 Legacy x86 和 compatibility mode)上都是有效的(valid)。
- 在 Intel processor 上:syscall/sysret 指令只能在 64-bit 模式上使用,compatibility 模式和 Legacy x86 模式上都是无效的。可是 sysret 指令虽然不能在 compatibility 模式下执行,但 sysret 却可以返回到 compaitibility 模式。这一点只能是认为了兼容 AMD 的 sysret 指令。
怎么办,这会不会出现兼容上的问题?这里有一个折衷的处理办法:
| 在 64 位环境里统一使用 syscall/sysret 指令,在 32 位环境里统一使用 sysenter/sysexit 指令 |
然而依旧会产生一些令人不愉快的顾虑:
| 没错,在 compatibility 模式下谁都不兼容谁: Intel 的 syscall/sysret 指令不能在 compatibility 模式下执行;AMD 的 sysenter/sysexit 指令也不能在 compatibility 模式下执行。 |
因此:在 compatibility 模式下必须切换到 64 位模式,然后使用 syscall/sysret 指令
1. syscall 指令的逻辑
下面是用 C 语言描述 syscall 指令执行的逻辑:
| MSR_EFER EFER; void syscall() if (EFER.LMA == 1) { /* long mode is active */ /* /* /* /* set rflags */ /* goto rip ... */
rcx = (unsigned long long)eip; /* eip extend to 64 load into rcx */ SS.selector = STAR.SYSCALL_CS + 8; rflags.VM = 0; /* goto rip */ } |
syscall 指令促使 processor 进行一系列的强制行为:
- 目标代码 DPL = 0,强制切换到 CPL = 0
- CS 被强制为 non-conforming,read/execute 属性,当 long mode 下,目标代码为 64 位,当 legacy mode 下目标代码为 32 位。
- SS 被强制为 expand-up,read/write 属性
- base 都被 0
- limit 都为 4G
在执行 syscall 指令前,processor 会检测 EFER 寄存器的 SCE 标志位,以确认是否开启 System Call Extension 功能,如果没有开启会产生 #UD 无效 opcode 码异常。
可以看出 syscall 指令只是加载 selector,但是并不进行实质的 descriptor 加载行为,强制设置 CS 和 SS 寄存器以满足系统服务例程(指 CPL=0 下的服务例程)的工作环境。
2. sysret 指令的逻辑
sysret 指令执行的情形有些稍复杂,我举些例子来说明。
2.1 从 64 位返回到 64 位
| bits 64 pop rcx ; restore rcx for rip db 0x48 ; 64-bit operand size for sysret !!! |
别看上面的汇编代码怪异,实际上它是最正常的代码,使用手工编码的方式是基于:确保编译器能够编译出我们想要的机器指令。
sysret 指令的缺省操作数是 32 位的,因此这里使用 REX prefix 将 sysret 指令的操作数调整到 64 位,现在我们的代码就返回到 64-bit 模式。这是我们想要的结果:从 64-bit Level-0 代码返回到 64-bit Level-3 代码。
2.2 从 64 位返回到 compatibility 模式
| bits 64 pop rcx ; restore rcx for rip sysret ; 32-bit operand size |
这里看上去很正常,其实不正常的汇编代码(不正常是指:不是我们想要的),我们让编译器来产生 sysret 指令机器码,那么编译器会产生 32 位操作数的机器指令。在这情形下,将会返回到 compatibility 模式,这样大多数情况下并不是我们想的结果。
有没有可能通过 sysret 返回到 legacy 模式?当然不可能。回到 legacy 模式要经过一系列的切换工作,这方面的工作,详见:http://www.mouseos.com/arch/exit_longmode.html
sysret 的逻辑如下:
| void sysret() if (CR0.PE == 0 || CS.attribute.DPL != 0) /* protected mode is disable or CPL != 0 */ if (CS.attribute.L == 1) /* 64-bit mode */ } else { rip = (unsigned long long)ecx; /* goto rip */ } else { /* compatibility or legacy mode */ CS.selector = STAR.SYSRET_CS; /* 32-bit code segment selector */ SS.selector = STAR.SYSRET_CS + 8; rflags.IF = 1; rip = (unsigned long long)ecx; } |
processor 同样要检查是否开启了 System Call Extension 功能,并且检查是否处于保护模式,当前的 CPL 是否为 0,和 syscall 指令一样,sysret 不做 descriptor 的加载工作,同样需要强制设置 CS 以满足用户环境(指的是 CPL=3 下的代码),可是:processor 并不设置 SS 寄存器,那么需要加载 data segment descriptor 进入 SS 寄存器
3. syscall/sysret 使用的寄存器
为了支持 syscall/sysret 指令,AMD 新增了4个 MSR 寄存器:
- STAR
- LSTAR
- CSTAR
- SFMASK

在 Intel 下 STSR 被称作 IA32_STAR,LSTAR 被称作 IA32_LSTAR, SFMASK 被称作 IA32_SFMASK, 虽然是冠以 IA32 体系,但是请相信它们是 64 位的。除前面所说的只能在 64 位环境执行,其它方面完全是兼容 AMD 的。
4. STAR 寄存器

通过上图我们已经明白了 STAR 寄存器的用途:
- 在 legacy x86 下提供 eip 值(仅在 legacy x86 模式下)
- 为 syscall 指令提供目标代码的 CS 和 SS selector
- 为 sysret 指令提供返回代码的 CS 和 SS selector
因此,STAR 寄存器分为三部分:
- [31:00] - SYSCALL_EIP
- [47:32] - SYSCALL_CS
- [63:48] - SYSRET_CS
STAR 寄存器的地址是 C0000081h,我们可以使用 wrmsr 指令来写 STAR 寄存器
下面是来自我的 mouseos 0.01 版中的初始化 syscall 环境的 init_syscall() 代码:
| init_syscall: mov edx, SYSCALL_CS | ((SYSRET_CS | 0x3) << 16) xor edx, edx |
在 ECX 寄存器中提供 MSR_STAR 的地址,64 位的值由 EDX:EAX 提供,高 32 位放在 EDX 寄存器,低 32 位放在 EAX 寄存器。
init_syscall 代码中分别设置了 STAR 和 LSTAR 以及 SFMASK 寄存器,这里 SFMASK 被设为 0
4.1 设置 SYSCALL_CS 以及 SYSCALL_SS
你应该让 SYSCALL_CS 提供索引 DPL=0 的 Code Segment Descriptor 的 selector。
| 注意: 尽管 processor 会忽略你提供的 SYSCALL_CS.RPL 值,而强制 SYSCALL_CS.RPL = 0,但是你必须将 SYSCALL_CS.RPL 设为 0。 那是因为:SYSCALL_SS 由 SYSCALL_CS + 8 而来! |
SYSCALL_SS = SYSCALL_CS + 8,这表示:目标代码的 SS descriptor 是 CS descriptor 的下一个 descriptor
下面同样是来自 mouseos 0.01 的代码,为 syscall 设置 descriptor
| ;-------------------------------------------------------------- kernel_ss_desc dd 0 ; 0x0c |
在这个代码中 SYSCALL_CS 设为 0x0b,那么 SYSCALL_SS 就是 0x0c,因此我们在设置 SYSCALL_CS 必须考虑到下一个应该是 SYSCALL_SS
4.2 设置 SYSRET_CS 以及 SYSRET_SS
同理,你也应该让 SYSRET_CS 提供索引 DPL=3 的 Code Segment Descriptor 的 selector,同样 SYSRET_SS = SYSRET_CS + 8,因此:你必须设置 SYSRET_CS.RPL = 3,以此来设置返回的 SYSRET_SS.RPL = 3。
注意,SYSRET_CS 提供的是 3 个 selector,分别是:
- 32-bit code 的 selector:用来返回到 compaitibility 模式代码,以及 legacy x86 模式代码
- SS selector:这个 selector 既是 32-bit 下的 selector,也是 64-bit 下的 selector,将会被加载到 SS 寄存器,descriptor 也会被加载。
- 64-bit code 的 selector:这个是用来返回到 64-bit 模式代码
那么:
- SYSRET_CS:32-bit code segment descriptor selector(包括 legacy x86 的 16-bit 代码)
- SYSRET_CS+8:stack segment descriptor selector
- SYSRET_CS+16:64-bit code segment descriptor selector
这里很容易让人产生疑问:为什么 code segment descriptor selector 分 32-bit(包括 legacy 16-bit)和 64-bit 两个,SS selector 只有一个?
实际上,这个问题很简单,很容易明白其中的道理,只要我们明白 data segment descriptor 的结构就知道了。

上图是 legacy mode 下的 data segment descriptor 结构,下图是 long mode 下的 data segment descriptor 结构(实际上不包括 compaitibility 模式),可以看出 data segment descriiptor 在 legacy 模式下与 long mode 模式下都是一样的。只是在 64-bit 模式下,绝大部分是无效的。
同一个 data segment descriptor 由 processor 当前的状态来决定是属于 legacy mode 下的 data segment descriptor 还是 64-bit 下的 data segment desciptor,因此,只提供一个 data segment descriptor selector 来加载到 SS 寄存器。
4.3 SYSRET_EIP
这部分是为 legacy mode 下提供的,当 processor 处于 32-bit protected mode 下,像这样:
| bits 32 ...... syscall ; 32-bit mode syscall! eip = MSR_STAR.SYSCALL_EIP |
在 32 位代码下执行 syscall 指令,目标的 eip 将从 STAR 的 SYSCALL_EIP 部分提取!
当然使用前需设置 SYSCALL_EIP 值:
| bits 32
mov eax, syscall32_entry ; 32-bit syscall entry ... ... |
5. LSTAR 寄存器
LSTAR 寄存器为 64-bit 代码的提供目标的 rip 值。
5.1 设置 LSTAR 寄存器
下面代码示范了设置 LSTAR 寄存器:
| bits 64
... ... mov rax, syscall64_entry ; 64-bit syscall entry ... ... |
64 位的 rip 值需要被分两部分,低 32 位放在 eax 寄存器,高 32 位放在 edx 寄存器,EDX:EAX 形成 64 位的 rip 值。必须注意的是:这个 64 位地址值是 canonical 形式的值,否则 wrmsr 会产生 #GP 异常。这段初始化代码虽然在 64 位执行,当然你可以在 32 位代码下对 MSR_LSTAR 完成初始化工作,这是正确的。
5.2 使用 LSTAR 寄存器
| bits 64
sys_service_call: ... ... syscall ; fast call system service routine, load rip from LSTAR ret |
当 processor 运行在 64-bit 模式下,执行 syscall 指令时,目标代码的 rip 将从 LSTAR 中加载。
5.3 返回 64-bit
| bits 64
sys_service_return: ... ... pop rcx ; restore rip db 0x48 ; operand size is 64-bit for return to 64-bit code |
前面说过,我们需使用 REX prefix 进行调整 sysret 指令的操作数,以它能够正确返回到 64-bit 代码,否则将会返回到 compatibility 模式。如果有需要应该恢复 rcx 和 r11 寄存器值。取决你有没有改变 rcx 和 r11 的值。因为 rip 和 rflags 需要从 rcx 和 r11 提取。
6. CSTAR 寄存器
CSTAR 寄存器为 compatibility 模式下的代码提供 rip 值,当 processor 在 comatibility 模式下运行时,执行了 syscall 指令,此时 rip 值将从 MSR_STAR 寄存器中加载。
请记住:只能在 AMD 的 processor 使用 compaitibility 模式下的调用。正如下面的代码:
| bits 32
sys_service_entry: ... ... syscall ; fast call system service routine, load rip from MSR_CSTAR ret |
执行 syscall 指令时,processor 处于 compaitibility 模式,那么 rip 将从 CSTAR 寄存器取得目标代码的 rip 值。实际上执行的结果是:会从 compatibility 模式切换到 64-bit 模式
那么,在系统的服务例程执行完毕后,使用 sysret 指令会从 64-bit 模式切换回 compatibility 模式继续执行,正如下面代码所示:
| bits 64
sys_service_return_to_compatibility: ... ... pop rcx ; restore for return eip sysret ; return to user's compatibility mode |
这段代码在 64-bit 模式下执行,但是返回到 compaitibility 模式。
6.1 通用性的考虑
照顾通用性,为了在 Intel 和 AMD 的 processor 上都能够使用 fast call 功能,操作系统的设计者应该要避免在 comaptibility 模式下使用 syscall 指令。前面提到过,建议在 compatibility 模式下先切换到 64-bit 模式后,再执行 syscall 指令。
正如下面的代码,在系统中提供切换的 stub 库功能:
6.1.1 切换到 64-bit
在 compatibility 模式下执行切换到 64-bit 模式,如下示例:
| bits 32
mov ecx, return_position ; save return position for compatibility mode jmp far sel64:sys_service_entry_stub ; 64-bit stub function |
这段代码先保存返回值到 ecx 寄存器,然然通过远跳转(jmp far)切换到 64 位代码,此时 processor 的权限是不变的。 切换后 CPL 还是 3
6.1.2 执行 fast call
现在正处于 64-bit 代码下,CPL=3,下面代码进行 fast call:
| bits 64 ;----------------------------------------------------
mov eax, ecx ; return position push sel32 ; 32-bit code selector db 0x48 ; operand size is 64-bit for retf |
为了返回原来的 compatibility 模式代码,我们可以使用 retf 指令切换回原来的 compatibility 模式,我们依次压入 32-bit selector 和返回地址,再执行我们需要的 syscall 指令。这样当使用 sysret 指令返回后,就可以执行 retf 指令切换回 compatibility 模式。
7. SFMASK 寄存器
在 long mode 下,当执行 syscall 指令时,当前的 rflags 寄存器值被保存在 r11 寄存器,processor 在执行 syscall 时,准备的目标执行环境中,rflags 将会根据 SFMASK 寄存器的值进行设置:如果 SFMASK 寄存器的某一位置为 1,那么 rflags 寄存器中相应的位将会被清 0,置为 0 时,rflags 寄存器中相应位不变
它的逻辑 C 描述为:
| rflags = rflags & (~sfmask); |
下面代码显示了如何使用 SFMASK 寄存器:
| init_syscall: ... ... xor eax, eax bts eax, 14 ; for rflags.NT = 0 mov ecx, 0C0000084h ; MSR_SFMASK address |
在这段初台化 SFMASK 寄存器代码中,将 SFMASK 的 bit 14 置为 1,这样的结果导致执行 syscall 指令后,rflags.NT 将会被清为 0(bit14 是 NT 标志)。
你应该在系统服务例程先保存 r11 值(原来的 rflags 寄存器值),以便 sysret 执行返回时可以恢复原来的 rflags 值。
本文详细解析了Linux系统调用的实现机制,重点介绍了read系统调用的具体流程,包括宏定义、多路汇聚过程及虚拟文件系统层的处理。
2903

被折叠的 条评论
为什么被折叠?



