內(nèi)核態(tài)與用戶態(tài)
早期工程師們在操作系統(tǒng)上編寫程序的時(shí)候,自己寫個(gè)程序可以訪問別人的程序地址,甚至是操作系統(tǒng)占用的地址,這樣就很容易一不小心就直接把操作系統(tǒng)給干掛了,所以那個(gè)時(shí)候的程序員編寫程序都得小心翼翼的
計(jì)算機(jī)核心的資源,一般有:內(nèi)存,I/O端口,特殊機(jī)器指令等,這些資源必須得保護(hù)起來,規(guī)定哪些程序可以去訪問,哪些程序不能去訪問
所以引入了特權(quán)級別的概念,由硬件設(shè)備商直接來提供硬件級別的支持,最常見的就是給CPU指令集的權(quán)限分級來控制CPU的訪問權(quán)限
比如 Intel CPU指令集操作的權(quán)限由高到低劃為4級:Ring0、Ring1、Ring2、Ring3,其中Ring0權(quán)限最高,可以使用所有CPU指令集,Ring3權(quán)限最低,僅能使用部分CPU指令,比如不能使用操作硬件資源的CPU指令:I/O操作、內(nèi)存分配等操作;另外CPU處于Ring3狀態(tài)不能訪問Ring0的地址空間,包括代碼和數(shù)據(jù)
CPU指令集,就是CPU中用來計(jì)算和控制計(jì)算機(jī)系統(tǒng)的一套指令的集合,實(shí)現(xiàn)軟件指揮硬件執(zhí)行的媒介,常見的CPU指令集有X86、ARM、MIPS、Alpha、RISC等
那么CPU是如何記錄這些特權(quán)級信息的?
我們這里以80386CPU為例,前文提到過CPU里面有許多段寄存器(CS、DS、SS、ES、FS、GS等)。這些段寄存器里面存放段選擇符(也叫段選擇子)

段選擇符中包含請求特權(quán)級RPL(CPL)字段,通過段選擇符可以去查找全局描述符表GDT、局部描述符表LDT中對應(yīng)的項(xiàng),需要先進(jìn)行特權(quán)級檢查;這些項(xiàng)中都包含DPL字段(規(guī)定訪問該段的權(quán)限級別),只有DPL >= max {CPL, RPL},才允許訪問
CPL很特殊,跟蹤當(dāng)前CPU正在執(zhí)行的代碼所在段的描述符中DPL的值,總是等于CPU的當(dāng)前特權(quán)級
內(nèi)核態(tài)與用戶態(tài)都是操作系統(tǒng)的層面的概念,和CPU硬件沒有必然的聯(lián)系;由于硬件已經(jīng)提供了一套特權(quán)級使用的相關(guān)機(jī)制,Linux操作系統(tǒng)沒有必要重新"造輪子",直接使用了硬件的Ring0和Ring3這兩個(gè)級別的權(quán)限,也就是使用Ring3作為用戶態(tài),Ring0作為內(nèi)核態(tài)
那么有人會問為什么Linux系統(tǒng)僅使用了Ring0和Ring3這兩個(gè)級別?
因?yàn)镃PU給的權(quán)限管理細(xì)度不夠,比如Intel CPU中Ring2和Ring3在操作系統(tǒng)里安全情況沒有區(qū)別,Ring1下的系統(tǒng)權(quán)限又需要經(jīng)常調(diào)用Ring0特權(quán)指令,頻繁切換特權(quán)級成本過高,操作系統(tǒng)不如將Ring2合并到Ring3,將Ring1劃入Ring0特權(quán)級
另一方面不是每種處理器都像x86一樣支持4個(gè)權(quán)限級別,有些處理器可能只支持2個(gè)級別,更少的特權(quán)級別,便于移植其他處理器架構(gòu)上
我們再來看下linux的體系架構(gòu)圖:

我們可以發(fā)現(xiàn)Linux系統(tǒng)從整體上看,被劃分為用戶態(tài)和內(nèi)核態(tài)
內(nèi)核態(tài)
內(nèi)核態(tài)是處于操作系統(tǒng)的最核心處,Ring0特權(quán)級,擁有操作系統(tǒng)的最高權(quán)限,能夠控制所有的硬件資源,掌控各種核心數(shù)據(jù),并且能夠訪問內(nèi)存中的任意地址;由內(nèi)核態(tài)統(tǒng)一管理這些核心資源,減少有限資源的訪問和使用沖突;在內(nèi)核里發(fā)生的任何程序異常都是災(zāi)難性的,會導(dǎo)致整個(gè)操作系統(tǒng)的奔潰
用戶態(tài)
用戶態(tài),就是我們通常編寫程序的地方,處于Ring3特權(quán)級,權(quán)限較低;這一層次的程序沒有對硬件的直接控制權(quán)限,也不能直接訪問地址的內(nèi)存。在這種模式下,即使程序發(fā)生崩潰也不會影響其他程序,可恢復(fù)
什么是系統(tǒng)調(diào)用
當(dāng)計(jì)算機(jī)啟動的時(shí)候,CPU處于Ring0狀態(tài),這個(gè)時(shí)候所有的指令都可以執(zhí)行,通過主引導(dǎo)程序?qū)⒋疟P扇區(qū)中的操作系統(tǒng)程序加載到內(nèi)存中,從而啟動操作系統(tǒng)(需要注意一下,本文的操作系統(tǒng) 以Linux0.12為例子)
也就是說當(dāng)Linux0.12啟動的時(shí)候,是在權(quán)限最高級別的內(nèi)核態(tài)運(yùn)行的;同時(shí)對內(nèi)存進(jìn)行劃分,劃出一部分(內(nèi)核區(qū))專門給內(nèi)核使用,這部分內(nèi)存只能被內(nèi)核使用;主內(nèi)存區(qū)域給其他應(yīng)用軟件使用。對這部分感興趣地,可以看看筆者之前的文章Linux0.12內(nèi)核源碼解讀(6)-main.c

當(dāng)操作系統(tǒng)啟動完成后,CPU就切換到Ring3級別上,操作系統(tǒng)同時(shí)進(jìn)入用戶態(tài),之后的應(yīng)用程序代碼都運(yùn)行在權(quán)限最低級別的用戶態(tài)上,通常我們能編寫的程序都運(yùn)行在用戶態(tài)上
需要格外注意一下,CPU特權(quán)級其實(shí)并不會對操作系統(tǒng)的用戶造成什么影響!有人會和Linux的用戶權(quán)限搞混淆,無論是根用戶(root),管理員,訪客還是一般用戶,它們都屬于用戶;而所有的用戶代碼都在用戶態(tài)Ring3上執(zhí)行,所有的內(nèi)核代碼都在內(nèi)核態(tài)Ring0上執(zhí)行,和Linux用戶的身份權(quán)限并沒有關(guān)系!
因?yàn)槲覀兙帉懙某绦蚨歼\(yùn)行在用戶態(tài)上,是無法對內(nèi)存和I/O端口的訪問,可以說基本上無法與外部世界交互,但是我們平時(shí)工作的時(shí)候訪問磁盤、寫文件,這些都是必要的需求,怎么辦?
那就需要通過執(zhí)行系統(tǒng)調(diào)用system call,操作系統(tǒng)會切換到內(nèi)核態(tài),由內(nèi)核去統(tǒng)一執(zhí)行相關(guān)操作(大哥幫小弟去執(zhí)行);當(dāng)執(zhí)行完操作系統(tǒng)再切換回用戶態(tài)。這樣方便集中管理,減少有限資源的訪問和使用沖突
系統(tǒng)調(diào)用是操作系統(tǒng)專門為用戶態(tài)運(yùn)行的進(jìn)程與硬件設(shè)備之間進(jìn)行交互提供了一組接口,是用戶態(tài)主動要求切換到內(nèi)核態(tài)的一種方式
系統(tǒng)調(diào)用是怎么實(shí)現(xiàn)的
接下來我們就結(jié)合Linux0.12的源碼一起來看看系統(tǒng)調(diào)用是怎么實(shí)現(xiàn)的?
庫函數(shù)write
本文以一個(gè)常見的庫函數(shù)write函數(shù)為例來,來更方便大家理解,開始發(fā)車:
?
?
//??lib/write.c #define?__LIBRARY__ #include??//頭文件 _syscall3(int,write,int,fd,const?char?*,buf,off_t,count)?//定義write的實(shí)現(xiàn),:fd -?文件描述符;buf -?寫緩沖區(qū)指針;count -?寫字節(jié)數(shù)
?
?
write.c這個(gè)文件主要是定義write的實(shí)現(xiàn),_syscall3(*,write,*)函數(shù)的主要功能是,向文件描述符fd指定的文件寫入count個(gè)字節(jié)的數(shù)據(jù)到緩沖區(qū)buf中
需要注意一下#define __LIBRARY__這個(gè)宏定義,這里定義直接原因是為了包括在unistd.h中的內(nèi)嵌匯編代碼
庫函數(shù)擴(kuò)展匯編宏
因?yàn)開syscall3這個(gè)函數(shù)定義在/include/unistd.h中,來看下源碼:
?
?
//??/include/unistd.h #ifdef?__LIBRARY__?#?若提前定義__LIBRARY__,則以后內(nèi)容被包含 ... #define?__NR_write?4?//系統(tǒng)調(diào)用號,用作系統(tǒng)調(diào)用函數(shù)表中索引值 ... //定義有3個(gè)參數(shù)的,?定義系統(tǒng)調(diào)用嵌入式匯編宏函數(shù) //%0?- eax(__res),%1 - eax(__NR_name),%2 - ebx(a),%3 - ecx(b),%4 - edx(c)。 #define?_syscall3(type,name,atype,a,btype,b,ctype,c)? type?name(atype?a,btype?b,ctype?c)? {? long?__res;? __asm__?volatile?("int?$0x80"??????????????????????????????????????????????//?調(diào)用系統(tǒng)中斷?0x80 ?:?"=a"?(__res)???????????????????????????????????????????????????????????//?返回值eax(__res) ?:?"0"?(__NR_##name),"b"?((long)(a)),"c"?((long)(b)),"d"?((long)(c)));????//輸入為:系統(tǒng)中斷調(diào)用號__NR_name,還有另外3個(gè)參數(shù) if?(__res>=0)??????????????????????????????????????????????????????????????//?如果返回值>=0,則直接返回該值 ?return?(type)?__res;? errno=-__res;??????????????????????????????????????????????????????????????//?否則置出錯號,并返回-1 return?-1;????????????????????????????????????????????????????????????????? } #endif?/*?__LIBRARY__?*/ ... int?write(int?fildes,?const?char?*?buf,?off_t?count);?//write系統(tǒng)調(diào)用的函數(shù)原型定義 ...
?
?
只有在lib/write.c中先定義了#define __LIBRARY__,那么才能在/include/unistd.h中,找到系統(tǒng)調(diào)用號和內(nèi)嵌匯編_syscall3();不然就代表它不需要進(jìn)行系統(tǒng)調(diào)用,這樣就可以忽略unistd.h中和系統(tǒng)調(diào)用相關(guān)的宏定義,非常的優(yōu)雅
其實(shí)我們可以把write.c中的write函數(shù)再重新整合一下:
?
?
int?write(int?fd,const?char*?buf,off_t?count)?
{?
long?__res;?
__asm__?volatile?(?"int?$0x80"?
:?"=a"?(__res)?
:?""?(__NR_write),?"b"?((long)(fd)),?"c"?((long)(buf)),?"d"?((long)(count)));?
if?(__res>=0)?
return?(type)?__res;?
errno=-__res;?
return?-1;?
}
?
?
這樣大家就能更容易明白#define __LIBRARY__的作用
上面int $0x80"表示調(diào)用系統(tǒng)中斷0x80 ** ,其實(shí)系統(tǒng)調(diào)用的本質(zhì)還是通過中斷(0x80)去實(shí)現(xiàn)的**!操作系統(tǒng)中真的是處處離不開中斷。中斷相關(guān)知識不了解的,可以看看筆者之前寫過的一篇文章圖解計(jì)算機(jī)中斷
另外由于程序處于用戶態(tài)無法直接操作硬件資源,所以需要進(jìn)行系統(tǒng)調(diào)用,切換到內(nèi)核態(tài);也就是說用戶程序如果使用庫函數(shù)write,會進(jìn)行系統(tǒng)調(diào)用
而系統(tǒng)調(diào)用,其實(shí)就是去調(diào)用int 0x80中斷,然后把三個(gè)參數(shù)fd、buf、count依次存入ebx、ecx、edx寄存器
還有#define __NR_write4 ,定義了系統(tǒng)調(diào)用號;_NR_write會被存入eax寄存器;當(dāng)調(diào)用返回后,從eax取出返回值,存入__res,建立了用戶棧和內(nèi)核棧的聯(lián)系。至于__NR_write的作用下文再講解
int 0x80中斷 調(diào)用對應(yīng)的中斷處理函數(shù)
我們來看下中斷是調(diào)用對應(yīng)的中斷處理函數(shù)的流程圖:

當(dāng)發(fā)生中斷的時(shí)候,CPU獲取到中斷向量號后,通過IDTR,去查找IDT中斷描述符表,得到相應(yīng)的中斷描述符;然后根據(jù)描述符中的對應(yīng)中斷處理程序的入口地址,去執(zhí)行中斷處理程序
早在linux0.12啟動時(shí),會進(jìn)行調(diào)度程序初始化main.c/sched_init(),其源碼:
?
?
//?????/kernel/sched.c
...
void?sched_init(void)
{
?...
?set_system_gate(0x80,&system_call);//設(shè)置系統(tǒng)調(diào)用中斷門
}
...
?
?
set_system_gate在之前的文章Linux0.12內(nèi)核源碼解讀(7)-陷阱門初始化講解過,不再贅述
需要注意的是:在用戶態(tài)和內(nèi)核態(tài)運(yùn)行的進(jìn)程使用的棧是不同的,分別叫做用戶棧和內(nèi)核棧, 兩者各自負(fù)責(zé)相應(yīng)特權(quán)級別狀態(tài)下的函數(shù)調(diào)用;所以當(dāng)執(zhí)行系統(tǒng)調(diào)用中斷int 0x80從用戶態(tài)進(jìn)入內(nèi)核態(tài)時(shí),會從用戶棧切換到內(nèi)核棧,系統(tǒng)調(diào)用返回時(shí),還要切換回用戶棧,繼續(xù)完成用戶態(tài)下的函數(shù)調(diào)用(這也叫做被中斷進(jìn)程上下文的保存與恢復(fù))
其中其關(guān)鍵作用的是,CPU會可以自動通過TR寄存器找到當(dāng)前進(jìn)程的TSS,然后根據(jù)里面ss0和esp0的值找到內(nèi)核棧的位置,完成用戶棧到內(nèi)核棧的切換。先了解一下,這塊等進(jìn)程那塊我們會再詳細(xì)聊聊
set_system_gate(0x80,&system_call)這句整體作用是,設(shè)置系統(tǒng)調(diào)用中斷門,將0x80中斷和函數(shù)system_call綁定在一起,換句話說system_call就是0x80的中斷處理函數(shù)
檢索系統(tǒng)調(diào)用函數(shù)表
我們接著去看system_call函數(shù)的源碼:
?
?
//????/kernel/sys_call.s ... //?int?0x80 _system_call: ?push?%ds??????#?壓棧,?保存原段寄存器值 ?push?%es ?push?%fs??? ?pushl?%eax??#?保存eax原值 ?pushl?%edx?? ?pushl?%ecx??#?push?%ebx,%ecx,%edx?as?parameters ?pushl?%ebx??#?to?the?system?call,??ebx,ecx,edx?中放著系統(tǒng)調(diào)用對應(yīng)的C語言函數(shù)的參數(shù) ?movl?$0x10,%edx??#?ds,es?指向內(nèi)核數(shù)據(jù)段 ?mov?%dx,%ds ?mov?%dx,%es ?movl?$0x17,%edx??#?fs?指向當(dāng)前局部數(shù)據(jù)段(局部描述符表中數(shù)據(jù)段描述符) ?mov?%dx,%fs ?cmpl?_NR_syscalls,%eax??#?判斷eax是否超過了最大的系統(tǒng)調(diào)用號,調(diào)用號如果超出范圍的話就跳轉(zhuǎn)! ?jae?bad_sys_call ?call?_sys_call_table(,%eax,4)???#?間接調(diào)用指定功能C函數(shù)! ?pushl?%eax??????????????????????#??把系統(tǒng)調(diào)用的返回值入棧! ... ret_from_sys_call:??#當(dāng)系統(tǒng)調(diào)用執(zhí)行完畢之后,會執(zhí)行此處的匯編代碼,從而返回用戶態(tài) ?movl?_current,%eax??#?取當(dāng)前任務(wù)(進(jìn)程)數(shù)據(jù)結(jié)構(gòu)指針->eax ?cmpl?_task,%eax???#?task[0]?cannot?have?signals ?...
?
?
其中 _sys_call_table(,%eax,4),這里的eax寄存器存放的就是_NR_write系統(tǒng)調(diào)用號,_sys_call_table是sys.h中的一個(gè)int (*)()類型的數(shù)組,里面存的是所有的系統(tǒng)調(diào)用函數(shù)地址,也叫做系統(tǒng)調(diào)用函數(shù)表,所以__NR_write也表示系統(tǒng)調(diào)用函數(shù)表中的索引值
那為什么%eax * 4乘上4呢?這是因?yàn)閟ys_call_table[]指針每項(xiàng)4 個(gè)字節(jié),這樣被調(diào)用處理函數(shù)的地址=[_sys_call_table + %eax * 4]
我們再來看下sys_call_table的定義:
?
?
//????/include/linux/sys.h
...
extern?int?sys_write();
...
fn_ptr?sys_call_table[]?=?{?sys_setup,?sys_exit,?sys_fork,?sys_read,
sys_write,?sys_open,?sys_close,?sys_waitpid,?sys_creat,?sys_link,
sys_unlink,?sys_execve,?sys_chdir,?sys_time,?sys_mknod,?sys_chmod,
sys_chown,?sys_break,?sys_stat,?sys_lseek,?sys_getpid,?sys_mount,
sys_umount,?sys_setuid,?sys_getuid,?sys_stime,?sys_ptrace,?sys_alarm,
sys_fstat,?sys_pause,?sys_utime,?sys_stty,?sys_gtty,?sys_access,
sys_nice,?sys_ftime,?sys_sync,?sys_kill,?sys_rename,?sys_mkdir,
sys_rmdir,?sys_dup,?sys_pipe,?sys_times,?sys_prof,?sys_brk,?sys_setgid,
sys_getgid,?sys_signal,?sys_geteuid,?sys_getegid,?sys_acct,?sys_phys,
sys_lock,?sys_ioctl,?sys_fcntl,?sys_mpx,?sys_setpgid,?sys_ulimit,
sys_uname,?sys_umask,?sys_chroot,?sys_ustat,?sys_dup2,?sys_getppid,
sys_getpgrp,?sys_setsid,?sys_sigaction,?sys_sgetmask,?sys_ssetmask,
sys_setreuid,sys_setregid,?sys_sigsuspend,?sys_sigpending,?sys_sethostname,
sys_setrlimit,?sys_getrlimit,?sys_getrusage,?sys_gettimeofday,?
sys_settimeofday,?sys_getgroups,?sys_setgroups,?sys_select,?sys_symlink,
sys_lstat,?sys_readlink,?sys_uselib?};
//系統(tǒng)調(diào)用總數(shù)目,注意一下:這里相較于linux0.11做了改進(jìn),新增系統(tǒng)調(diào)用不再需要手動調(diào)整該數(shù)目!
int?NR_syscalls?=?sizeof(sys_call_table)/sizeof(fn_ptr);
?
?
可以知曉這里的call _sys_call_table(,%eax,4)就是調(diào)用系統(tǒng)調(diào)用號所對應(yīng)的內(nèi)核系統(tǒng)調(diào)用函數(shù)sys_write
最終執(zhí)行sys_write
sys_write在fs下的read_write.c:
?
?
//???/fs/read_write.c
//?寫文件系統(tǒng)調(diào)用
int?sys_write(unsigned?int?fd,char?*?buf,int?count)
{
?struct?file?*?file;
?struct?m_inode?*?inode;
??//判斷函數(shù)參數(shù)的有效性
?if?(fd>=NR_OPEN?||?count?<0?||?!(file=current->filp[fd]))
??return?-EINVAL;
?if?(!count)
??return?0;
??//?取文件相應(yīng)的i節(jié)點(diǎn)
?inode=file->f_inode;
??//?若是管道文件,并且是寫管道文件模式,則進(jìn)行寫管道操作
?if?(inode->i_pipe)
??return?(file->f_mode&2)?write_pipe(inode,buf,count):-EIO;
??//如果是字符設(shè)備文件,則進(jìn)行寫字符設(shè)備操作
?if?(S_ISCHR(inode->i_mode))
??return?rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);
??//?如果是塊設(shè)備文件,則進(jìn)行塊設(shè)備寫操作
?if?(S_ISBLK(inode->i_mode))
??return?block_write(inode->i_zone[0],&file->f_pos,buf,count);
??//?若是常規(guī)文件,則執(zhí)行文件寫操作
?if?(S_ISREG(inode->i_mode))
??return?file_write(inode,file,buf,count);
?printk("(Write)inode->i_mode=%06o
",inode->i_mode);
?return?-EINVAL;
}
?
?
至此庫函數(shù)write,進(jìn)行系統(tǒng)調(diào)用,最終調(diào)用了sys_write這個(gè)函數(shù)
我們再通過下圖回顧一下,整個(gè)系統(tǒng)調(diào)用的過程:

內(nèi)核態(tài)與用戶態(tài)數(shù)據(jù)交互
到這里我們已經(jīng)了解了系統(tǒng)調(diào)用的過程,還遺留一個(gè)問題需要去解決一下,就是內(nèi)核態(tài)與用戶態(tài)如何進(jìn)行數(shù)據(jù)交互?
回顧系統(tǒng)調(diào)用過程中,我們可以發(fā)現(xiàn)寄存器在其中起到了不可或缺的作用,linus在linux0.12中也是采用類似的方法來進(jìn)行數(shù)據(jù)交互
我們這里繼續(xù)以sys_write函數(shù)為例,來看看里面的file_write(inode,file,buf,count);
?
?
//???/fs/file_dev.c
//?寫文件函數(shù)?-?根據(jù)?i?節(jié)點(diǎn)和文件結(jié)構(gòu)信息,將用戶數(shù)據(jù)寫入文件中
int?file_write(struct?m_inode?*?inode,?struct?file?*?filp,?char?*?buf,?int?count)
{
?off_t?pos;
?int?block,c;
?struct?buffer_head?*?bh;
?char?*?p;
?int?i=0;
/*
?*?ok,?append?may?not?work?when?many?processes?are?writing?at?the?same?time
?*?but?so?what.?That?way?leads?to?madness?anyway.
?*/
?//如果設(shè)置了追加標(biāo)記位,則更新當(dāng)前位置指針到文件最后一個(gè)字節(jié)
?if?(filp->f_flags?&?O_APPEND)
??pos?=?inode->i_size;
?else
??pos?=?filp->f_pos;
??//?i為已經(jīng)寫入的長度,count為需要寫入的長度
?while?(ii_dev,block)))
???break;
??c?=?pos?%?BLOCK_SIZE;
??p?=?c?+?bh->b_data;//?開始寫入數(shù)據(jù)的位置
??bh->b_dirt?=?1;?//標(biāo)記數(shù)據(jù)需要回寫硬盤
??c?=?BLOCK_SIZE-c;?//算出能寫的長度
??if?(c?>?count-i)?c?=?count-i;
??pos?+=?c;
??if?(pos?>?inode->i_size)?{
???inode->i_size?=?pos;
???inode->i_dirt?=?1;
??}
??i?+=?c;
??while?(c-->0)
???*(p++)?=?get_fs_byte(buf++);//從用戶態(tài)拷貝一個(gè)字節(jié)的數(shù)據(jù)到內(nèi)核態(tài)
??brelse(bh);
?}
??//當(dāng)數(shù)據(jù)已經(jīng)全部寫入文件或者在寫操作過程中發(fā)生問題時(shí)就會退出循環(huán)
?inode->i_mtime?=?CURRENT_TIME;
?if?(!(filp->f_flags?&?O_APPEND))?{
??filp->f_pos?=?pos;
??inode->i_ctime?=?CURRENT_TIME;
?}
?return?(i?i:-1);
}
?
?
我們這里不展開講了,得后面講完磁盤和文件系統(tǒng)再回過頭來講講這塊,把目光聚焦于get_fs_byte函數(shù),我們來看下其源碼:
?
?
//??include/asm/segment.h
?
?//?讀取 fs 段中指定地址處的字節(jié)。
?//?參數(shù):addr -?指定的內(nèi)存地址。
?//?%0?-?(返回的字節(jié)_v);%1 -?(內(nèi)存地址 addr)。
?//?返回:返回內(nèi)存 fs:[addr]處的字節(jié)。
?//?第 3 行定義了一個(gè)寄存器變量_v,該變量將被保存在一個(gè)寄存器中,以便于高效訪問和操作。
extern?inline?unsigned?char?get_fs_byte(const?char?*?addr)
{
?unsigned?register?char?_v;
?__asm__?("movb?%%fs:%1,%0":"=r"?(_v):"m"?(*addr));
?return?_v;
}
?//?將一字節(jié)存放在 fs 段中指定內(nèi)存地址處。
?//?參數(shù):val -?字節(jié)值;addr -?內(nèi)存地址。
?//?%0?-?寄存器(字節(jié)值 val);%1 -?(內(nèi)存地址 addr)。
extern?inline?void?put_fs_byte(char?val,char?*addr)
{
__asm__?("movb?%0,%%fs:%1"::"r"?(val),"m"?(*addr));
}
?
?
get_fs_byte函數(shù)是從用戶態(tài)拷貝一個(gè)字節(jié)的數(shù)據(jù)到內(nèi)核態(tài),而put_fs_byte則恰恰相反,從內(nèi)核態(tài)拷貝一個(gè)字節(jié)的數(shù)據(jù)到用戶態(tài)
在系統(tǒng)調(diào)用運(yùn)行整個(gè)過程中,DS和ES段寄存器指向內(nèi)核數(shù)據(jù)空間,而FS段寄存器被設(shè)置為指向用戶數(shù)據(jù)空間,這可能有人會問為啥?
別忘了在/kernel/sys_call.s中_system_call中的這段:
?
?
_system_call: ... ?movl?$0x10,%edx??#?ds,es?指向內(nèi)核數(shù)據(jù)段 ?mov?%dx,%ds ?mov?%dx,%es ?movl?$0x17,%edx??#?fs?指向當(dāng)前局部數(shù)據(jù)段(局部描述符表中數(shù)據(jù)段描述符) ?mov?%dx,%fs ...
?
?
0x10是全局描述符表GDT中內(nèi)核數(shù)據(jù)段描述符的段值,0x17是局部描述符表LDT中的任務(wù)的數(shù)據(jù)段描述符的段值
所以linux這里利用FS寄存器來完成內(nèi)核數(shù)據(jù)空間與用戶數(shù)據(jù)空間之間的數(shù)據(jù)復(fù)制,當(dāng)進(jìn)程從中斷調(diào)用中退出時(shí),寄存器會自動從內(nèi)核棧彈出,快捷高效
審核編輯:黃飛
?
電子發(fā)燒友App
































評論