Linux 函數(shù)調(diào)用的用戶態(tài)與內(nèi)核態(tài)
在用戶態(tài)中,程序的執(zhí)行往往是一個(gè)函數(shù)調(diào)用另一個(gè)函數(shù)。函數(shù)調(diào)用都是通過(guò)棧來(lái)進(jìn)行的。
在進(jìn)程的內(nèi)存空間里面,棧是一個(gè)從高地址到低地址,往下增長(zhǎng)的結(jié)構(gòu),也就是上面是棧底,下面是棧頂,入棧和出棧的操作都是從下面的棧頂開(kāi)始的。
32 位操作系統(tǒng)在 CPU 里,ESP(Extended Stack Pointer)是棧頂指針寄存器,入棧操作 Push 和出棧操作 Pop 指令,會(huì)自動(dòng)調(diào)整 ESP 的值。另外有一個(gè)寄存器 EBP(Extended Base Pointer),是棧基地址指針寄存器,指向當(dāng)前棧幀的最底部。
例如,A 調(diào)用 B,A 的棧里面包含 A 函數(shù)的局部變量,然后是調(diào)用 B 的時(shí)候要傳給它的參數(shù),然后返回 A 的地址,這個(gè)地址也應(yīng)該入棧,這就形成了 A 的棧幀。接下來(lái)就是 B 的棧幀部分了,先保存的是 A 棧幀的棧底位置,也就是 EBP。因?yàn)樵?B 函數(shù)里面獲取 A 傳進(jìn)來(lái)的參數(shù),就是通過(guò)這個(gè)指針獲取的,接下來(lái)保存的是 B 的局部變量等等。
當(dāng) B 返回的時(shí)候,返回值會(huì)保存在 EAX 寄存器中,從棧中彈出返回地址,將指令跳轉(zhuǎn)回去,參數(shù)也從棧中彈出,然后繼續(xù)執(zhí)行 A。
對(duì)于 64 位操作系統(tǒng),模式多少有些不一樣。因?yàn)?64 位操作系統(tǒng)的寄存器數(shù)目比較多。rax 用于保存函數(shù)調(diào)用的返回結(jié)果。棧頂指針寄存器變成了 rsp,指向棧頂位置。堆棧的 Pop 和 Push 操作會(huì)自動(dòng)調(diào)整 rsp,棧基指針寄存器變成了 rbp,指向當(dāng)前棧幀的起始位置。
改變比較多的是參數(shù)傳遞。rdi、rsi、rdx、rcx、r8、r9 這 6 個(gè)寄存器,用于傳遞存儲(chǔ)函數(shù)調(diào)用時(shí)的 6 個(gè)參數(shù)。如果超過(guò) 6 的時(shí)候,還是需要放到棧里面。
然而,前 6 個(gè)參數(shù)有時(shí)候需要進(jìn)行尋址,但是如果在寄存器里面,是沒(méi)有地址的,因而還是會(huì)放到棧里面,只不過(guò)放到棧里面的操作是被調(diào)用函數(shù)做的。
以上的棧操作,都是在進(jìn)程的內(nèi)存空間里面進(jìn)行的。
當(dāng)系統(tǒng)調(diào)用從用戶態(tài)到內(nèi)核態(tài)的時(shí)候,首先要做的第一件事情,就是將用戶態(tài)運(yùn)行過(guò)程中的 CPU 上下文保存起來(lái),其實(shí)主要就是保存在這個(gè)結(jié)構(gòu)的寄存器變量里。這樣當(dāng)從內(nèi)核系統(tǒng)調(diào)用返回的時(shí)候,才能讓進(jìn)程在剛才的地方接著運(yùn)行下去。
在用戶態(tài),應(yīng)用程序進(jìn)行了至少一次函數(shù)調(diào)用。32 位和 64 的傳遞參數(shù)的方式稍有不同,32 位的就是用函數(shù)棧,64 位的前 6 個(gè)參數(shù)用寄存器,其他的用函數(shù)棧。
在內(nèi)核態(tài),32 位和 64 位都使用內(nèi)核棧,格式也稍有不同,主要集中在 pt_regs 結(jié)構(gòu)上。
在內(nèi)核態(tài),32 位和 64 位的內(nèi)核棧和 task_struct 的關(guān)聯(lián)關(guān)系不同。32 位主要靠 thread_info,64 位主要靠 Per-CPU 變量。