2018-2019-1 20165202 《資訊安全系統設計基礎》第七週學習總結
2018-2019-1 20165202 《資訊安全系統設計基礎》第七週學習總結
學習目標
1.瞭解異常及其種類
2.理解程序和併發的概念
3.掌握程序建立和控制的系統呼叫及函式使用:fork,exec,wait,waitpid,exit,getpid,getppid,sleep,pause,setenv,unsetenv,
4.理解陣列指標、指標陣列、函式指標、指標函式的區別
5.理解訊號機制:kill,alarm,signal,sigaction
6.掌握管道和I/O重定向:pipe, dup, dup2
- 控制流:控制轉移序列。
- 控制轉移:從一條指令到下一條指令。
- 異常控制流:現代作業系統通過使控制流發生突變來對系統狀態做出反應,這些突變稱為異常控制流。
異常
異常的一部分由硬體實現,一部分由作業系統實現,它就是控制流中的突變,用來響應處理器狀態的某些變化。注意和語言中的應用級的異常概念區分。
處理器中,狀態被編碼為不同的位和訊號,狀態變化被稱為事件,事件不一定和當前指令的執行有關。處理器檢測到有事件發生時,會通過異常表進行間接過程呼叫,到一個專門設計處理事件的作業系統子程式,稱為異常處理程式。
異常處理程式完成處理後,根據異常事件的型別會(執行一種):
- 將控制返回給當前指令(事件發生時正在執行的)。
- 將控制返回給下一條指令(沒有異常將會執行的)。
- 終止被中斷的程式。
異常表是一張跳轉表,表目k包含異常k的處理程式的地址,在系統啟動時由作業系統分配和初始化。系統中每種可能的異常都分配了一個唯一的非負整數的異常號。
異常類似過程呼叫,不同的是:
- 過程呼叫跳轉前會將返回地址壓入棧中,但異常的返回地址只能是當前指令或下一條指令。
- 處理器會把一些額外的重新開始被中斷程式需要的處理器狀態壓入棧中。
- 控制從使用者程式轉到核心,這些專案壓入核心棧中,而不是使用者棧。
- 異常處理程式執行在核心模式下(對系統資源有完全訪問許可權)。
異常可以分為四類:
類別 | 原因 | 非同步/同步 | 返回行為 |
---|---|---|---|
中斷 | 來自I/O裝置的訊號 | 非同步 | 總是返回到下一條指令 |
陷阱 | 有意的異常 | 同步 | 總是返回到下一條指令 |
故障 | 潛在可恢復錯誤號 | 同步 | 可能返回到當前指令 |
終止 | 不可恢復錯誤 同步 | 同步 | 不返回 |
非同步異常是由處理器外部的I/O裝置中的事件產生的,同步異常是執行一條指令的產物。
中斷是非同步發生的,硬體中斷不由任何指令造成,所以說是非同步的。硬體中斷的異常處理程式稱為中斷處理程式。
陷阱、故障和終止是同步發生的,稱為故障指令。
陷阱是有意的異常,主要用來在使用者程式和核心之間提供一個像過程一樣的介面,稱為系統呼叫。處理器提供了 syscall n 指令來滿足使用者向核心請求服務 n , syscall 指令會導致一個到異常處理程式的陷阱,處理程式呼叫適當的核心程式。普通函式執行在使用者模式,而系統呼叫執行在核心模式。
故障由錯誤引起,如缺頁異常。故障發生時,處理器將控制轉移給故障處理程式,如果處理程式能夠修正錯誤,就將控制返回到故障指令,重新執行;否則處理程式返回到核心的 abort 例程, abort 終止應用程式。
終止是不可恢復的致命錯誤的結果,主要是一些硬體錯誤。終止處理程式將控制返回到 abort 例程,abort 終止應用程式。
程序
程序是一個執行中程式的例項。系統中每個程式都是執行在某個程序的上下文中的。上下文由程式正確執行所需的狀態組成,包括程式的存放在儲存器中的程式碼和資料、棧、通用目的暫存器的內容、程式計數器、環境變數和開啟檔案描述符的集合。
在shell中執行程式時,shell會建立一個新的程序,然後在新程序的上下文中執行可執行目標檔案。應用程式還能建立新程序。
程序給應用程式提供了兩個關鍵抽象:
- 獨立的邏輯控制流,提供程式獨佔處理器的假象。
- 私有的地址空間,提供程式獨佔儲存器系統的假象。
邏輯控制流
程式執行的一系列PC(程式計數器)值唯一地對應於包含在程式的可執行目標檔案中的指令或包含在執行時動態連結的共享庫中的指令,這個PC值的序列稱為邏輯控制流。
程序輪流使用處理器,每個程序執行它的流的一部分,然後被搶佔,其他程序開始執行。程式執行在程序的上下文中,因此像是在獨佔地使用處理器。
邏輯流是相互獨立的,程序互不影響。可以通過程序間通訊(IPC)機制來實現程序間互動。
邏輯流在時間上和其他邏輯流重疊的程序稱為併發程序,這兩個程序稱為併發執行。如A和B、A和C,而B和C不是併發執行的。
程序執行控制流的一部分的時間段稱為時間片,程序和其他程序輪換執行稱為多工,也稱時間分片。
私有地址空間
程序為每個程式提供私有地址空間,和這個空間中某地址相關聯的儲存器位元組不能被其他程序讀寫。和私有地址空間關聯的儲存器內容一般不同,但空間有相同的結構。前一篇Linux執行時儲存器映像給出了Linux程序的地址空間結構。
使用者模式和核心模式
需要限制一個應用可以執行的指令以及可訪問的地址空間範圍來實現程序抽象,通過特定控制暫存器的一個模式位來提供這種機制。設定了模式位時,程序執行在核心模式中,程序可以執行任何指令和訪問任何儲存器位置。沒設定模式位時,程序執行在使用者模式中,程序不允許執行特權指令和訪問地址空間中核心區內的程式碼和資料。使用者程式必須通過系統呼叫介面間接地訪問核心程式碼和資料。
使用者程式的程序初始是在使用者模式中的,必須通過中斷、故障或陷入系統呼叫這樣的異常來變為核心模式。
Linux有一種 /proc 檔案系統,包含核心資料結構的內容的可讀形式,執行使用者模式程序訪問。
上下文切換
核心為每個程序維持一個上下文,它是核心重新啟動一個被搶佔程序所需的狀態。包括通用目的暫存器、浮點暫存器、程式計數器、使用者棧、狀態暫存器、核心棧和各種核心資料結構(頁表、程序表和檔案表等)的值。
核心通過上下文切換來實現多工,它是一種高階的異常控制流,建立在低階異常機制上。
核心決定搶佔當前程序,重新開始一個先前被搶佔的程序,稱為排程了一個新程序,由核心中的排程器程式碼處理。使用上下文切換來將控制轉移到新程序。上下文切換儲存當前程序的上下文,恢復先前被搶佔程序儲存的上下文,將控制傳遞給新恢復的程序。
系統呼叫和中斷可以引發上下文切換。
系統呼叫
在Linux中,可以使用 man syscalls 檢視全部系統呼叫的列表。系統級函式遇到錯誤時,通常返回-1,並設定全域性變數 errno 。
strace 命令可以列印程式和它的子程序呼叫的每個系統呼叫的軌跡
程序控制
程序有三種狀態:
- 執行。程序在CPU上執行,或等待被執行(會被排程)。
- 停止。程序被掛起(不會被排程)。收到 SIGSTOP 、 SIGTSTP 、 SIDTTIN 、 SIGTTOU 訊號,程序停止,收到 SIGCONT 訊號,程序再次開始執行。
- 終止。程序永遠停止。原因可能是:收到終止程序的訊號,從主程式返回,呼叫 exit 函式。
建立新程序可以使用 fork 函式。新建立的子程序和父程序幾乎相同,它獲得父程序使用者級虛擬地址空間和檔案描述符的副本,主要區別是它們的PID不同。 fork 函式呼叫一次,返回兩次;父子程序是併發執行的,不能假設它們的執行順序;兩個程序的初始地址空間相同,但是是相互獨立的;它們還共享開啟的檔案。
因為有相同的程式程式碼,所以如果呼叫 fork 三次,就會有八個程序。
程序終止時,並不會被立即清除,而是等待父程序回收,稱為僵死程序。父程序回收終止的子程序時,核心將子程序退出狀態傳給父程序,然後拋棄該程序。如果回收前父程序已經終止,那麼僵死程序由 init 程序回收。
回收子程序可以用 wait 和 waitpid 等函式。
核心呼叫 execve 函式在當前程序的上下文中載入並執行一個新程式。 execve 載入 filename 後,呼叫啟動程式碼,啟動程式碼準備棧,將控制傳給新程式的主函式 int main(int argc, char *argv[], char *envp[])
。下面是使用者棧的典型組織:
引數陣列和環境陣列會被傳遞給程式。按C標準, main 函式只有兩個引數,一般環境陣列使用全域性變數 environ 傳遞。
操作環境陣列可以通過 getenv 函式族。
#include <stdlib.h>
/** 在環境陣列中搜索字串"name=value"
* @return 返回指向value的指標,若無返回NULL */
char *getenv(const char *name);
/** 以"name=value"格式取字串,新增到陣列
* @return 返回0,出錯返回-1 */
int putenv(char *string);
/** 若name不存在,將"name=value"新增到陣列;如存在且overwrite非0,用value覆蓋原值
* @return 返回0,出錯返回-1 */
int setenv(const char *name, const char *value, int overwrite);
/** 刪除環境變數name
* @return 返回0,出錯返回-1 */
int unsetenv(const char *name);
name
為環境變數名。
程式是程式碼和資料的集合,可以作為目標模組存在於磁碟,或作為段存在於地址空間中。程序是執行中程式的一個例項。程式總是執行在某個程序的上下文中。
ps
命令可以檢視系統中當前的程序。
top
命令會列印當前程序資源使用的資訊。
訊號
訊號是一種更高層軟體形式的異常,它允許程序中斷其他程序。一個訊號即一條資訊,通知程序一個某種型別的事件已經在系統中發生了。
下表是Linux系統中的訊號:
號碼 | 名字 | 預設行為 | 相應事件 |
---|---|---|---|
1 | SIGHUP | 終止 | 終端線掛起 |
2 | SIGINT | 終止 | 來自鍵盤的中斷 |
3 | SIGQUIT | 終止 | 來自鍵盤的退出 |
4 | SIGILL | 終止 | 非法指令 |
5 | SIGTRAP | 終止並轉儲儲存器 | 跟蹤陷阱 |
6 | SIGABRT | 終止並轉儲儲存器 | 來自 abort 函式的終止訊號 |
7 | SIGBUS | 終止 | 匯流排錯誤 |
8 | SIGFPE | 終止並轉儲儲存器 | 浮點異常 |
9 | SIGKILL | 終止 | 殺死程式 |
10 | SIGUSR1 | 終止 | 使用者定義的訊號1 |
11 | SIGSEGV | 終止 | 並轉儲儲存器 無效的儲存器引用(段故障) |
12 | SIGUSR2 | 終止 | 使用者定義的訊號2 |
13 | SIGPIPE | 終止 | 向一個沒有讀使用者的管道做些操作 |
14 | SIGALRM | 終止 | 來自 alarm 函式的定時器訊號 |
15 | SIGTERM | 終止 | 軟體終止訊號 |
16 | SIGSTKFLT | 終止 | 協處理器上的棧故障 |
17 | SIGCHLD | 忽略 | 一個子程序停止或終止 |
18 | SIGCONT | 忽略 | 使停止程序繼續 |
19 | SIGSTOP | 停止直到下一個 SIGCONT | 不來自終端的暫停訊號 |
20 | SIGTSTP | 停止直到下一個 SIGCONT | 來自終端的暫停訊號 |
21 | SIGTTIN | 停止直到下一個 SIGCONT | 後臺程序從終端讀 |
22 | SIGTTOU | 停止直到下一個 SIGCONT | 後臺程序向終端寫 |
23 | SIGURG | 忽略 | 套接字上的緊急情況 |
24 | SIGXCPU | 終止 | CPU時間限制超出 |
25 | SIGXFSZ | 終止 | 檔案大小限制超出 |
26 | SIGVTALRM | 終止 | 虛擬定時器期滿 |
27 | SIGPROF | 終止 | 剖析定時器期滿 |
28 | SIGWINCH | 忽略 | 視窗大小變化 |
29 | SIGIO | 終止 | 在某個描述符上可執行I/O操作 |
30 | SIGPWR | 終止 | 電源故障 |
每種訊號型別都對應某個型別的系統事件。底層硬體異常通常對使用者程序不可見,訊號提供了一種機制向用戶程序通知這些異常的發生。其他訊號對應核心或其他使用者程序中較高層的軟體事件。
傳送訊號指核心通過更新目的程序上下文中的某個狀態,傳送一個訊號給目的程序。傳送訊號的原因有:
- 核心檢測到一個系統事件,如除零或子程序終止。
- 程序呼叫了 kill 函式,顯示要求核心傳送訊號給目的程序。程序可以給自己傳送訊號。
接收訊號指目的程序被核心強迫以某種方式對訊號的傳送做出反應。程序可以忽略訊號,終止,或執行訊號處理程式捕獲訊號。
發出而沒有被接收的訊號稱為待處理訊號。一種型別最多有一個待處理訊號,重複的訊號被丟棄。程序可以阻塞某種訊號,這時仍可被髮送,但不會被接收。一個待處理訊號最多隻能被接收一次。
傳送訊號
傳送訊號給程序基於程序組的概念。程序組由一個正整數ID標識,每個程序只屬於一個程序組。
前面章節提到過作業的概念,shell為每個作業建立一個獨立的程序組,程序組ID一般為作業中父程序中的一個。
傳送 SIGINT 訊號到shell,shell捕獲訊號傳送給前臺程序組的每個程序,預設終止前臺作業。 ^Z 傳送 SIGTSTP 訊號到shell,shell捕獲訊號傳送給前臺程序組的每個程序,預設掛起前臺作業。
用 kill 命令向其他程序傳送任意訊號,給定的PID為負值時,表示傳送訊號給程序組ID為PID絕對值的所有程序。
程序可以用 kill函式傳送訊號給任意程序(包括自己)。
接收訊號
每個程序都有一個訊號遮蔽字,它規定了當前要阻塞遞送到該程序的訊號集。每個可能的訊號都有一位遮蔽字,對應位設定時表明訊號當前是被阻塞的。用 sigprocmask 函式檢測和更改當前訊號遮蔽字。
核心從異常處理程式返回,將控制傳遞給程序p之前會檢查未被阻塞的待處理訊號的集合。集合為空則核心傳遞控制給程序p的邏輯控制流的下一條指令;集合非空則核心選擇集合中某個訊號k(通常取最小k),強制程序p接收k。訊號觸發程序的某種行為,程序完成行為後控制傳遞給p的邏輯控制流的下一條指令。
每種訊號都有預設行為,可以用 signal 函式修改和訊號關聯的預設行為(除 SIGSTOP 和 SIGKILL 外):
#include <signal.h>
typedef void (*sighandler_t)(int);
/** 改變和訊號signum關聯的行為
* @return 返回前次處理程式的指標,出錯返回SIG_ERR */
sighandler_t signal(int signum, sighandler_t handler);
引數說明:
signum
訊號編號。
handler
指向使用者定義函式,也就是訊號處理程式的指標。或者為:
- SIG_IGN :忽略 signum 訊號。
- SIG_DFL :恢復 signum 訊號預設行為。
訊號處理程式的呼叫稱為捕捉訊號,訊號處理程式的執行稱為處理訊號。 signal 函式會將 signum 引數傳遞給訊號處理程式 handler 的引數,這樣 handler 可以捕捉不同型別的訊號。
signal 的語義和實現有關,最好使用 sigaction 函式代替它。
例:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
void w_error(const char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
void handler(int sig)
{
printf("caught SIGINT\n");
/* exit(0); */
}
int main()
{
if (signal(SIGINT, handler) == SIG_ERR)
w_error("signal error");
pause();
printf("come back\n");
exit(0);
}
訊號處理
前面已經指出,不會有重複的訊號排隊等待。訊號處理有以下特性:
- 訊號處理程式阻塞當前正在處理的型別的待處理訊號。
- 同種型別至多有一個待處理訊號。
- 會潛在阻塞程序的慢速系統呼叫被訊號中斷後,在訊號處理程式返回時不再繼續,而返回一個錯誤條件,並將 errno 設為 EINTR 。
對於第三點,Linux系統會重啟系統呼叫,而Solaris不會。不同系統之間,訊號處理語義存在差異。Posix標準定義了 sigaction 函式,使在Posix相容的系統上可以設定訊號處理語義。
非本地跳轉
C提供了一種使用者級的異常控制流,稱為非本地跳轉。它將控制直接從一個函式轉移到另一個正在執行的函式。
#include <setjmp.h>
/** 在env緩衝區中儲存當前棧的內容,供longjmp使用,返回0
* @return setjmp返回0,longjmp返回非0 */
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);
/** 從env緩衝區中恢復棧的內容,觸發一個從最近一次初始化env的setjmp呼叫的返回,setjmp返回非0的給定val值 */
void longjmp(jmp_buf env, int val);
void siglongjmp(sigjmp_buf env, int val);
非本地跳轉可以用來從一個深層巢狀的函式呼叫中立即返回,如檢測到錯誤;或者使一個訊號處理程式轉移到一個特殊的程式碼位置,而不是返回到訊號中斷的指令的位置。
在訊號處理程式中進行非本地跳轉時應使用 sigsetjmp 和 siglongjmp 。如果 savesigs 非0,則 sigsetjmp 在 env 中儲存程序的當前訊號遮蔽字,呼叫 siglongjmp 時從 env 恢復儲存的訊號遮蔽字。同時,應該使用一個 volatile sig_atomic_t 型別的變數來確保 env 未設定時不被中斷。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
static sigjmp_buf buf;
static volatile sig_atomic_t canjmp;
void handler(int sig)
{
if (canjmp == 0)
return;
/* ... */
canjmp = 0;
siglongjmp(buf, 1);
}
int main()
{
signal(SIGINT, handler);
if (!sigsetjmp(buf, 1))
printf("starting\n");
else
printf("restarting\n");
canjmp = 1;
while (1) {
sleep(1);
printf("processing ...\n");
}
exit(0);
}
學習進度條
程式碼行數(新增/累積) | 部落格量(新增/累積) | 學習時間(新增/累積) | 重要成長 | |
---|---|---|---|---|
目標 | 5000行 | 20篇 | 400小時 | |
第一週 | 100/100 | 1/1 | 5/5 | |
第二週 | 100/200 | 2/3 | 5/10 | |
第三週 | 100/300 | 2/5 | 5/15 | |
第四周 | 100/400 | 1/6 | 5/20 | |
第六週 | 100/500 | 1/7 | 5/25 | |
第七週 | 100/600 | 1/8 | 5/30 | |
第八週 | 800/1400 | 3/11 | 10/40 | |
第九周 | 1000/2400 | 3/14 | 15/55 | |
第十一週 | 300/2700 | 2/16 | 20/75 | |
第十三週 | 100/2800 | 2/18 | 20/95 | |
第十四周 | 200/3000 | 1/19 | 20/115 |
參考資料
《深入理解計算機系統》異常控制流——讀書筆記202 《資訊安全系統設計基礎》第七週學習總結