堆棧變化過程:html
Linux內核分析——計算機是如何工做的linux
計算機是如何工做的?(總結)——三個法寶git
存儲程序計算機工做模型,計算機系統最最基礎性的邏輯結構;程序員
函數調用堆棧,高級語言得以運行的基礎,只有機器語言和彙編語言的時候堆棧機制對於計算機來講並不那麼重要,但有了高級語言及函數,堆棧成爲了計算機的基礎功能;github
enter 算法
pushl %ebpshell
movl %esp,%ebp編程
leave 數據結構
movl %ebp,%esp架構
popl %ebp
函數參數傳遞機制和局部變量存儲
中斷,多道程序操做系統的基點,沒有中斷機制程序只能從頭一直運行結束纔有可能開始運行其餘程序。
反彙編
gcc -g 生成可執行文件
objdump -S 得到反彙編文件
C語言內嵌彙編語言(模板):
asm volatile(
輸出:
輸入:
破壞描述部分
);
1、Linux內核源代碼介紹
一、根目錄
arch/x86目錄下的代碼是咱們重點關注的,arch中包括支持不一樣CPU的源代碼。
init目錄下包含內核啓動相關的代碼,如main.c(start_kernel函數至關於普通C程序的main函數,是Linux內核初始化的起點)。
ipc:進程間通訊
kernel:Linux內核的核心代碼
關注readme文件
2、構造一個簡單的Linux系統MenuOS
一、在實驗樓環境下:
cd LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
便可啓動內核,完成後進入menu程序,支持三個命令help、version和quit。
二、使用本身的Linux系統環境搭建MenuOS的過程
# 下載內核源代碼編譯內核
cd ~/LinuxKernel/
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
xz -d linux-3.18.6.tar.xz
tar -xvf linux-3.18.6.tar(解壓)
cd linux-3.18.6
make i386_defconfig
make # 通常要編譯很長時間,少則20分鐘多則數小時
# 製做根文件系統
cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git # 若是被牆,可使用附件menu.zip
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static –lpthread(init是第一個用戶態進程,是1號進程,採用的是靜態編譯的方式)
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img(img鏡像文件)
# 啓動MenuOS系統
cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
三、從新配置編譯Linux使之攜帶調試信息
(1)在原來配置的基礎上,make menuconfig選中以下選項從新配置Linux,使之攜帶調試信息
kernel hacking—>
[*] compile the kernel with debug info
(2)make從新編譯(時間較長)
四、使用gdb跟蹤調試內核
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 關於-s和-S選項的說明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,則可使用-gdb tcp:xxxx來取代-s選項
另開一個shell窗口
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中target remote以前加載符號表 file home/shiyanlou/LinuxKernel/vmlinux
(gdb)target remote:1234 # 創建gdb和gdbserver之間的鏈接,按c 讓qemu上的Linux繼續運行
(gdb)break start_kernel # 斷點的設置能夠在target remote以前,也能夠在以後
Linux內核分析實驗二:mykernel實驗指導(操做系統是如何工做的)
Linux內核分析實驗三----跟蹤分析Linux內核的啓動過程
1、用戶態、內核態
權限分級——爲了系統自己更穩定,使系統不宜崩潰。(並非全部程序員縮寫的代碼都很健壯!!)
x86 CPU四種不一樣的執行級別:0(內核態)—3(用戶態)
區分方法:CS:EIP(CPU的每條指令都是經過這裏執行)(代碼段選擇寄存器:偏移量寄存器)
CS寄存器的最低兩位代表了代碼級別——邏輯地址(不是物理地址)
內核態:能夠訪問4G地址空間的任意地址 用戶態:0x00000000—0xbfffffff空間內的地址
2、中斷——從用戶態進入內核態
系統調用是一種特殊的中斷(用戶態進程與硬件設備的接口)
切換時就會涉及到上下文的問題?
即中斷int指令在堆棧上會保存一些值:1.用戶態棧頂地址 2.當時狀態字 3.當時CS:EIP的值
save—cs:eip ss:eip eflag
load—cs:eip ss:eip
過程:1.SAVE_ALL——保存現場
判斷是否有進程調度?
(無進程調度)last.RESTORE_ALL+iret(popl cs:eip/ss:eip/efalg)
(有進程調度)last.暫時保存當前的值直到該進程再次被調度
3、API——應用程序編程接口(不一樣於系統調用)
rm menu -rf強制刪除原menu文件
git clone http:
makecd menurootfs運行自動編譯腳本,生成根文件系統,啓動MenuOS
更新menu代碼到最新版
test.c中main函數裏,增長MenuConfig
增長對應的兩個函數:Time和TimeAsm
make rootfs
-3.18.6/arch//boot/qemu -kernel linuxx86bzImage -initrd rootfs.img -s -S
gdb
-3.18.6/vmlinux (gdb)file linux
remote:1234 (gdb)target
b sys_time:在系統調用time的位置設置斷點
c:在MenuOs裏使用time,停在斷點處
s 單步調試—一步一步執行(PS:沒法執行time_asm中的int 0X80部分)
SAVE_ALL:保存現場
call *sys_call_table(,%eax,4)調用了系統調度處理函數,eax存的是系統調用號,是實際的系統調度程序。
sys_call_table:系統調用分派表
syscall_after_all:保存返回值
如有sys_exit_work,則進入sys_exit_work:會有一個進程調度時機。
work_pending -> work_notifysig,用來處理信號
可能call schedule:進程調度代碼
可能跳轉到restore_all,恢復現場。
若無sys_exit_work,就執行restore_all恢復,返回用戶態。
INTERRUPT_RETURN <=> iret,結束。
- - - 在系統調用返回以前,可能發生進程調度,進程調度裏就會出現進程上下文的切換。進程間通訊可能有信號須要處理。能夠將內核視爲一系列中斷指令的集合。
進程控制塊PCB——task_struct(進程描述符)
爲了管理進程,內核必須對每一個進程進行清晰的描述,進程描述符提供了內核所需瞭解的進程信息。
struct task_struct數據結構很龐大
Linux進程的狀態與操做系統原理中的描述的進程狀態彷佛有所不一樣,好比就緒狀態和運行狀態都是TASK_RUNNING,爲何呢?
TASK_RUNN有沒有在CPU上執行決定他是就緒仍是運行狀態。
和操做系統相似:就緒態,運行態,阻塞態
進程的標示pid
全部進程鏈表struct list_head tasks;
SMP條件編譯器
鏈表代碼——雙向鏈表
程序建立的進程具備父子關係,在編程時每每須要引用這樣的父子關係。進程描述符中有幾個域用來表示這樣的關係
Linux爲每一個進程分配一個8KB大小的內存區域,用於存放該進程兩個不一樣的數據結構:Thread_info和進程的內核堆棧
進程處於內核態時使用, 不一樣於用戶態堆棧,即PCB中指定了內核棧,那爲何PCB中沒有用戶態堆棧?用戶態堆棧是怎麼設定的?
內核控制路徑所用的堆棧 不多,所以對棧和Thread_info 來講,8KB足夠了
struct thread_struct thread; //CPU-specific state of this task
gcc hello.c -o hello.exe
gcc編譯源代碼生成最終可執行的二進制程序,GCC後臺隱含執行了四個階段步驟。
預處理 => 編譯 => 彙編 => 連接
預處理:編譯器將C源代碼中包含的頭文件編譯進來和執行宏替換等工做。
gcc -E hello.c -o hello.i
編譯:gcc首先要檢查代碼的規範性、是否有語法錯誤等,以肯定代碼的實際要作的工做,在檢查無誤後,gcc把代碼翻譯成彙編語言。
gcc –S hello.i –o hello.s -S:該選項只進行編譯而不進行彙編,生成彙編代碼。
彙編:把編譯階段生成的.s文件
轉成二進制目標代碼.
gcc –c hello.s –o hello.o
連接:將編譯輸出.o文件
連接成最終的可執行文件。
gcc hello.o –o hello
運行:若連接沒有-o指明,則生成可執行文件默認爲a.out
./hello
a.out——COFF——ELF(Linux)或PE(Windows)
重點介紹ELF——三種重要格式文件
列出/usr/bin下的目錄信息
$ ls -l /usr/bin
Shell自己不限制命令行參數的個數,命令行參數的個數受限於命令自身
int main(int argc, char *argv[], char *envp[])
Shell會調用execve將命令行參數和環境參數傳遞給可執行程序的main函數
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
庫函數exec*都是execve的封裝例程
shell程序 => execve => sys_execve
初始化新程序堆棧時拷貝進去
關注:load_elf_binary
load_elf_binary(...) { ... kernel_read();//其實就是文件解析 ... //映射到進程空間 0x804 8000地址 elf_map();// ... if(elf_interpreter) //依賴動態庫的話 { ... //裝載ld的起點 #得到動態鏈接器的程序起點 elf_entry=load_elf_interp(...); ... } else //靜態連接 { ... elf_entry = loc->elf_ex.e_entry; ... } ... //static exe: elf_entry: 0x804 8000 //exe with dyanmic lib: elf_entry: ld.so addr start_thread(regs,elf_entry,bprm->p); }
主要過程是動態連接器完成、用戶態完成。
/*準備.so文件*/ shlibexample.h (1.3 KB) - Interface of Shared Lib Example shlibexample.c (1.2 KB) - Implement of Shared Lib Example /*編譯成libshlibexample.so文件*/ $ gcc -shared shlibexample.c -o libshlibexample.so -m32 /*使用庫文件(由於已經包含了頭文件因此能夠直接調用函數)*/ SharedLibApi();
dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example /*編譯成libdllibexample.so文件*/ $ gcc -shared dllibexample.c -o libdllibexample.so -m32 /*使用庫文件*/ void * handle = dlopen("libdllibexample.so",RTLD_NOW);//先加載進來 int (*func)(void);//聲明一個函數指針 func = dlsym(handle,"DynamicalLoadingLibApi");//根據名稱找到函數指針 func(); //調用已聲明函數
$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32 $ export LD_LIBRARY_PATH=$PWD /*將當前目錄加入默認路徑,不然main找不到依賴的庫文件,固然也能夠將庫文件copy到默認路徑下。*/
execve和fork都是特殊一點的系統調用:通常的都是陷入到內核態再返回到用戶態。
fork兩次返回,第一次返回到父進程繼續向下執行,第二次是子進程返回到ret_from_fork而後正常返回到用戶態。
execve執行的時候陷入到內核態,用execve中加載的程序把當前正在執行的程序覆蓋掉,當系統調用返回的時候也就返回到新的可執行程序起點。
- 執行到可執行程序 -> 陷入內核 - 構造新的可執行文件 -> 覆蓋掉原可執行程序 - 返回到新的可執行程序,做爲起點(也就是main函數) - 須要構造其執行環境;
load_elf_interp
莊周(調用execve的可執行程序)入睡(調用execve陷入內核),醒來(系統調用execve返回用戶態)發現本身是蝴蝶(被execve加載的可執行程序)。
static struct linux_binfmt elf_format//聲明一個全局變量 = { .module = THIS_MODULE, .load_binary = load_elf_binary,//觀察者自動執行 .load_shlib = load_elf_library, .core_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE, }; static int __iit init_elf_binfmt(void) {n register_binfmt(&elf_format);//把變量註冊進內核鏈表,在鏈表裏查找文件的格式 return 0; }
1、進程調度與進程調度的時機分析
一、不一樣類型的進程有不一樣的調度需求
Linux既支持普通的分時進程,也支持實時進程。
Linux中的調度是多種調度策略和調度算法的混合。
二、調度策略:是一組規則,它們決定何時以怎樣的方式選擇一個新進程運行。
Linux的調度基於分時和優先級。
三、內核中的調度算法相關代碼使用了相似OOD的策略模式。
四、進程調度的時機
(1)中斷處理過程當中,schedule函數實現調度:
中斷處理過程(包括時鐘中斷、I/O中斷、系統調用和異常)中,直接調用schedule(),或者返回用戶態時根據need_resched標記調用schedule()。
(2)用戶態進程只能被動調度
用戶態進程沒法實現主動調度,僅能經過陷入內核態後的某個時機點進行調度,即在中斷處理過程當中進行調度。
(3)內核線程是隻有內核態沒有用戶態的特殊進程
內核線程能夠直接調用schedule()進行進程切換,也能夠在中斷處理過程當中進行調度,也就是說內核線程做爲一類的特殊的進程能夠主動調度,也能夠被動調度。
2、進程上下文切換相關代碼分析
一、爲了控制進程的執行,內核必須有能力掛起正在CPU上執行的進程,並恢復之前掛起的某個進程的執行,這叫作進程切換、任務切換、上下文切換。
二、掛起正在CPU上執行的進程,與中斷時保存現場是不一樣的,中斷先後是在同一個進程上下文中,只是由用戶態轉向內核態執行;進程上下文的切換是兩個進程的切換。
三、進程上下文包含了進程執行須要的全部信息
(1)用戶地址空間:包括程序代碼,數據,用戶堆棧等。
(2)控制信息:進程描述符,內核堆棧等。
(3)硬件上下文(注意中斷也要保存硬件上下文只是保存的方法不一樣)。
四、schedule()函數選擇一個新的進程來運行,並調用context_switch進行上下文的切換,這個宏調用switch_to來進行關鍵上下文切換。
next = pick_next_task(rq, prev);//進程調度算法、策略都封裝這個函數內部
context_switch(rq, prev, next);//完成進程上下文切換
switch_to利用了prev和next兩個參數:prev指向當前進程,next指向被調度的進程。
3、Linux系統的通常執行過程分析
一、最通常的狀況:正在運行的用戶態進程X切換到運行用戶態進程Y的過程
(1)正在運行的用戶態進程X
(2)發生中斷——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
(3)SAVE_ALL //保存現場
(4)中斷處理過程當中或中斷返回前調用了schedule(),其中的switch_to作了關鍵的進程上下文切換
(5)標號1以後開始運行用戶態進程Y(這裏Y曾經經過以上步驟被切換出去過所以能夠從標號1繼續執行)
(6)restore_all //恢復現場
(7)iret - pop cs:eip/ss:esp/eflags from kernel stack
(8)繼續運行用戶態進程Y
二、關鍵:中斷上下文的切換和進程上下文的切換
4、Linux系統執行過程當中的幾個特殊狀況
一、幾種特殊狀況
(1)經過中斷處理過程當中的調度時機,用戶態進程與內核線程之間互相切換和內核線程之間互相切換,與最通常的狀況很是相似,只是內核線程運行過程當中發生中斷沒有進程用戶態和內核態的轉換,CS段沒有變化;
(2)內核線程主動調用schedule(),只有進程上下文的切換,沒有發生中斷上下文的切換,與最通常的狀況略簡略;
(3)建立子進程的系統調用在子進程中的執行起點next_ip = ret_from_fork及返回用戶態,如fork;
(4)加載一個新的可執行程序後返回到用戶態的狀況,如execve。
二、X86_32位系統下,每一個進程的地址空間4G。0-3G用戶態,3G-4G僅內核態。
全部的進程3G以上的部分是共享的。
內核是各類中斷處理過程和內核線程的集合。
5、Linux操做系統架構概覽
一、操做系統的基本概念
二、典型的Linux操做系統的結構
6、最簡單也是最複雜的操做---執行ls命令
7、從CPU和內存的角度看Linux系統的執行
一、從CPU的角度
0xc0000000如下是3G的部分,用戶態。
(1)c=gets();系統調用,陷入內核態,將eip/esp/cs/ds等信息壓棧。
(2)進程管理:等待鍵盤敲入指令。
(3)中斷處理:在鍵盤上敲擊ls發生I/O中斷。
進程x陷入內核態後沒有內容執行變成阻塞態,發生I/O中斷後變成就緒態。
(4)系統調用返回。
二、從內存的角度