2017-2018-1 20179202《Linux內核原理與分析》第八周作業
一 、可執行程序的裝載
1. 預處理、編譯、鏈接
gcc –e –o hello.cpp hello.c //預處理
gcc -x cpp-output -S -o hello.s hello.cpp //編譯
gcc -x assembler -c hello.s -o hello.o-m32 //匯編
gcc -o hello hello.o //鏈接成可執行文件,使用共享庫
用gcc -o hello.static hello.o -static
靜態編譯出來的hello.static把C庫裏需要的東西也放到可執行文件裏了。用命令ls –l
,可以看到hello只有7K,hello.static有大概700K。
2. ELF文件
ELF(Excutable and Linking Format)是一個文件格式的標準。通過readelf-h hello
查看可執行文件hello的頭部(-a查看全部信息,-h只查看頭部信息),頭部裏面註明了目標文件類型ELF32。Entry point address是程序入口,地址為0x8048310,
即可執行文件加載到內存中開始執行的第一行代碼地址。頭部後還有一些代碼數據等等。可執行文件的格式和進程的地址空間有一個映射的關系,當程序要加載到內存中運行時,將ELF文件的代碼段和數據段加載到進程的地址空間。
ELF文件裏面三種目標文件:可重定位(relocatable)文件保存著代碼和適當的數據,用來和其它的object文件一起來創建一個可執行文件或者是一個共享文件(主要是.o文件);可執行(executable)文件保存著一個用來執行的程序,該文件指出了exec(BA_OS)如何來創建程序進程映象(操作系統怎麽樣把可執行文件加載起來並且從哪裏開始執行);共享object文件保存著代碼和合適的數據,用來被兩個鏈接器鏈接。第一個是鏈接編輯器(靜態鏈接),可以和其它的可重定位和共享object文件來創建其它的object。第二個是動態鏈接器,聯合一個可執行文件和其它的共享object文件來創建一個進程映象。
3. 動態鏈接
動態鏈接有可執行裝載時的動態鏈接(大多數)和運行時的動態鏈兩種方式。
(1)共享庫
shlibexample.h中定義了SharedLibApi()函數,shlibexample.c是對此函數的實現。用`gcc -shared shlibexample.c -o libshlibexample.so -m32
(在64位環境下執行時加上-32)生成.so文件。這樣就生成了共享庫文件。
#include <stdio.h> #include "shlibexample.h" int SharedLibApi() { printf("This is a shared libary!\n"); return SUCCESS; }
(2)動態加載共享庫
dllibexample.h定義了DynamicalLoadingLibApi()函數,dllibexample.c是對此函數的實現。同樣使用gcc -shared dllibexample.c -o libdllibexample.so
得到動態加載共享庫。
#include <stdio.h>
#include "dllibexample.h"
#define SUCCESS 0
#define FAILURE (-1)
int DynamicalLoadingLibApi()
{
printf("This is a Dynamical Loading libary!\n");
return SUCCESS;
}
(3)main函數使用兩種動態鏈接庫。
#include <stdio.h>
#include "shlibexample.h"
#include <dlfcn.h>
int main()
{
printf("This is a Main program!\n");
/* Use Shared Lib */
printf("Calling SharedLibApi() function of libshlibexample.so!\n");
SharedLibApi(); //直接調用共享庫
/* Use Dynamical Loading Lib */
void * handle = dlopen("libdllibexample.so",RTLD_NOW);//打開動態庫並將其加載到內存
if(handle == NULL)
{
printf("Open Lib libdllibexample.so Error:%s\n",dlerror());
return FAILURE;
}
int (*func)(void);
char * error;
func = dlsym(handle,"DynamicalLoadingLibApi");
if((error = dlerror()) != NULL)
{
printf("DynamicalLoadingLibApi not found:%s\n",error);
return FAILURE;
}
printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n");
func();
dlclose(handle); //卸載庫
return SUCCESS;
}
可以看到main函數中只include了shlibexample(共享庫),沒有include dllibexample(動態加載共享庫),但是include了dlfcn。因為前面加了共享庫的接口文件,所以可以直接調用共享庫。但是如果要調用動態加載共享庫,就要使用定義在dlfcn.h中的dlopen。
用gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
生成可執行文件。註意,這裏只提供shlibexample的-L,並沒有提供dllibexample的相關信息,只是指明了-ldl。-dl動態加載,編譯main.c的時候,沒有指明任何相關信息,只是在程序內部指明了。實驗截圖如下:
3. 代碼分析
當前的可執行程序在執行,執行到execve的時候陷入到內核態,用execve的加載的可執行文件把當前進程的可執行程序給覆蓋掉,當execve的系統調用返回的時候,已經返回的不是原來的那個可執行程序了,是新的可執行程序的起點(main函數)。shell環境會執行execve,把命令行參數和環境變量都加載進來,當系統調用陷入到內核裏面的時候,system call調用sys_execve。sys_execve中調用了do_execve。
//sys_execve
SYSCALL_DEFINE3(execve,
const char __user *, filename, //可執行程序的名稱
const char __user *const __user *, argv, //程序的參數
const char __user *const __user *, envp) //環境變量
{
return do_execve(getname(filename), argv, envp);
}
//do_execve
int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execve_common(filename, argv, envp);
}
很明顯,繼續分析其中調用的do_execve_common:
static int do_execve_common(struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp)
{
struct linux_binprm *bprm;
struct file *file;
struct files_struct *displaced;
int retval;
...
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);//在堆上分配一個linux_binprm結構體
...
file = do_open_exec(filename);//打開需要加載的可執行文件,file中就包含了打開的可執行文件的信息
...
bprm->file = file; //賦值file指針
bprm->filename = bprm->interp = filename->name;//賦值文件名
retval = bprm_mm_init(bprm); //創建進程的內存地址空間
...
bprm->argc = count(argv, MAX_ARG_STRINGS);//賦值參數個數
...
bprm->envc = count(envp, MAX_ARG_STRINGS);//賦值環境變量個數
...
retval = copy_strings_kernel(1, &bprm->filename, bprm); //從內核空間獲取文件路徑;
...
bprm->exec = bprm->p; //p為當前內存頁最高地址
retval = copy_strings(bprm->envc, envp, bprm);//把環境變量拷貝到bprm中
...
retval = copy_strings(bprm->argc, argv, bprm);//把命令行參數拷貝到bprm中
...
retval = exec_binprm(bprm);//處理可執行文件
...
return retval;
}
linux_binprm結構體用來保存要執行文件的相關信息, 如文件的頭128字節、文件名、命令行參數、環境變量、文件路徑、內存描述符信息等。exec_binprm函數保存當前的pid,其中ret = search_binary_handler(bprm);
調用 search_binary_handler 尋找可執行文件的相應處理函數。
int search_binary_handler(struct linux_binprm *bprm)
{
bool need_retry = IS_ENABLED(CONFIG_MODULES);
struct linux_binfmt *fmt;
int retval;
...
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) { //遍歷文件解析鏈表
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
//解析elf格式執行的位置
retval = fmt->load_binary(bprm);// 加載可執行文件的處理函數
read_lock(&binfmt_lock);
...
}
return retval;
linux_binfmt結構體定義了一些函數指針,不同的Linux可接受的目標文件格式(如load_binary,load_shlib,core_dump)采用不同的函數來進行目標文件的裝載。每一個linux_binfmt結構體對應一種二進制程序處理方法。這些結構體實例會通過init_elf_binfmt
以註冊的方式加入到內核對應的format鏈表中去,通過register_binfmt()
和unregister_binfmt()
在鏈表中插入和刪除對象。
struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);//用於加載一個新的進程(通過讀取可執行文件中的信息)
int (*load_shlib)(struct file *); //用於動態加載共享庫
int (*core_dump)(struct coredump_params *cprm);//在core文件中保存當前進程的上下文
unsigned long min_coredump;
};
目標文件的格式是ELF,所以retval = fmt->load_binary(bprm);
中load_binary實際上調用load_elf_binary完成ELF二進制映像的認領、裝入和啟動。load_elf_binary這個函數指針被包含在一個名為elf_format的結構體中:
static structlinux_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,
};
全局變量elf_format賦給了一個指針,在init_elf_binfmt裏把變量註冊註冊到文件解析鏈表中,就可以在鏈表裏找到相應的文件格式。繼續分析load_elf_binary:
static int load_elf_binary(struct linux_binprm *bprm)
{
...
if (elf_interpreter) { // 動態鏈接的處理
...
} else { // 靜態鏈接的處理
elf_entry =loc->elf_ex.e_entry;
...
}
}
...
//將ELF文件映射到進程空間中,execve系統調用返回用戶態後進程就擁有了新的代碼段、數據段。
current->mm->end_code = end_code;
current->mm->start_code =start_code;
current->mm->start_data =start_data;
current->mm->end_data = end_data;
current->mm->start_stack =bprm->p;
...
start_thread(regs, elf_entry, bprm->p);
}
ELF文件中的Entry point address字段指明了程序入口地址,這個地址一般是0x8048000(0x8048000以上的是內核段內存)。該入口地址被解析後存放在elf_ex.e_entry中,所以靜態鏈接程序的起始位置就是elf_entry。這個函數中還有一個關鍵點start_thread:
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0);
regs->fs = 0;
regs->ds = __USER_DS;
regs->es = __USER_DS;
regs->ss = __USER_DS;
regs->cs = __USER_CS;
regs->ip = new_ip;
regs->sp = new_sp;
regs->flags = X86_EFLAGS_IF;
set_thread_flag(TIF_NOTIFY_RESUME);
}
regs中為系統調用時SAVE_ALL宏壓入內核棧的部分。new_ip的值等於參數elf_entry的值,即把ELF文件中定義的main函數起始地址賦值給eip寄存器,進程返回到用戶態時的執行位置從原來的int 0x80的下一條指令變成了new_ip的位置。
總結一下,調用順序是sys_execve -> do_execve -> do_execve_common -> exec_binprm,當系統調用從內核態返回到用戶態時,eip直接跳轉到ELF程序的入口地址,CPU也得到新的用戶態堆棧(包含新程序的命令行參數和shell上下文環境)。這樣,新程序就開始執行了。
4.靜態鏈接可執行文件的調試
用test_exe.c覆蓋test.c,增加了一句MenuConfig()執行一個程序。
int Exec(int argc, char *argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
printf("This is Child Process!\n");
execlp("/hello","hello",NULL);
}
else
{
/* parent process */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
makefile做了一些修改,編譯了hello.c,在生成根文件系統的時候,把init和hello都放到rootfs.img內。這樣在執行execve時就自動的加載hello可執行文件:
在前面分析的關鍵點設置斷點,一邊一句一句向下跟蹤,一邊對照執行過程。追蹤到start_thread
,用po new_ip
,得到的是0x804887f。
用readelf –h hello
可以看到這個可執行程序它的入口點地址也是0x804887f。
5.遇到的問題及解決
(1)看了視頻後對動態鏈接的第二種方式依然理解模糊,通過搜索資料解決。
??如果要調用動態加載共享庫,就要使用定義在dlfcn.h中的dlopen。給出文件名libdllibexample.so和標誌RTLD_NOW打開動態鏈接庫,返回handle句柄。dlsym函數與上面的dlopen函數配合使用,根據操作句柄(由dlopen打開動態鏈接後返回的指針)handle與符號(要求獲取的函數或全局變量的名稱)DynamicLoadingLibApi,返回符號對應的地址。使用此地址可以獲得庫中特定函數的地址,並且調用庫中的相應函數。這樣就可以使用動態加載共享庫裏面所定義的函數了。
(2)不理解調試中的po
po是print_object的縮寫,不僅僅可以輸出顯示定義的對象,也可以輸出表達式的結果。我嘗試了p、po、p/d、p\x,對比它們的執行結果:
可以發現p、p/d(10進制)、p\x(16進制)輸出值前都會有一個類似"$1="的前綴,它們是變量,在後面的表達式中可以使用,而po並不能把它的返回值存儲到變量裏。至於po還能在哪些地方看的不太清,以後遇到了再具體分析。
(3)在第六周實驗中,Rename函數實現把"hello.c"重命名為"newhello.c",在當前文件夾中放一個hello.c文件即可實現。但在MenuOS上,把hello.c文件嘗試放在menu文件夾下,執行rename命令顯示不成功:
所以我一直不知道該把hello.c文件放在哪裏才可以重命名成功。這周孟老師修改Makefile文件提醒了我,我修改了Makefile,把hello.c打包到鏡像文件中:
雖然顯示執行成功,不幸的是hello.c並沒有重命名為newhello.c:
我想,修改的應該是rootfs.img中的hello.c,所以這裏的hello.c才沒被修改(不知道思考的對不對,想打開rootfs.img,試了幾種方法都沒有解決)。
二 、課本筆記
虛擬文件系統
1.虛擬文件系統(VFS)是linux內核和存儲設備之間的抽象層。VFS中有四個主要的對象類型,分別是超級塊對象、索引節點對象、目錄項對象、文件對象。
2.超級塊主要存儲特定文件系統相關的信息,存儲在磁盤上,在使用時創建在內存中的。對於磁盤文件系統來說,這個對象通常對應磁盤上的一個文件系統控制塊(磁盤super block)。
3.索引節點包含內核在操作文件或目錄時需要的全部信息。一個索引節點代表文件系統中的一個文件(這裏的文件不僅是指我們平時所認為的普通的文件,還包括目錄,特殊設備文件等等)。索引節點存儲在磁盤上,當被應用程序訪問到時才會在內存中創建。
4.通過索引節點已經可以定位到指定的文件,但索引節點對象的屬性非常多,在查找,比較文件時,直接用索引節點效率不高,所以引入了目錄項(dentry)的概念。目錄項並不實際存在於磁盤上,在使用的時候在內存中創建目錄項對象。
5.在一個文件路徑中,路徑中的每一部分都被稱為目錄項。每個目錄項對象都有被使用,未使用和負狀態3種狀態。一個被使用的目錄項對應一個有效的索引節點,並且該對象由一個或多個使用者;一個未被使用的目錄項對應一個有效的索引節點,但是VFS當前並沒有使用這個目錄項;一個負狀態的目錄項沒有對應的有效索引節點。
6.在Linux中,除了普通文件,其他諸如目錄、設備、套接字等也以文件被對待即“一切皆文件”。文件對象表示進程已打開的文件,從用戶角度來看,我們在代碼中操作的就是一個文件對象。雖然一個文件對應的文件對象不是唯一的,但其對應的索引節點和目錄項對象卻是唯一的。
7.VFS中還有2個專門針對文件系統的2個對象,struct file_system_type
用來描述各種特定文件系統類型(比如ext3,ext4或UDF),struct vfsmount
用來描述一個安裝文件系統的實例。被Linux支持的文件系統,都有且僅有一 個file_system_type結構而不管它有零個或多個實例被安裝到系統中。當文件系統被實際安裝時,會在安裝點創建一個vfsmount結構體。
8.以下3個結構體和進程緊密聯系在一起:
- struct files_struct:由進程描述符中的 files 目錄項指向,所有與單個進程相關的信息(比如打開的文件和文件描述符)都包含在其中。
- struct fs_struct:由進程描述符中的 fs 域指向,包含文件系統和進程相關的信息。
- struct mmt_namespace:由進程描述符中的 mmt_namespace 域指向。
塊I/O層
1.I/O設備主要有字符設備和塊設備,相比字符設備的只能順序讀寫設備中的內容,塊設備能夠隨機讀寫設備中的內容。字符設備只能順序訪問,塊設備隨機訪問。
2.塊設備最小的可尋址單元是扇區。扇區的大小一般是2的整數倍,最常見的大小是512個字節。扇區是所有塊設備的基本單元,塊設備無法對比它還小的單元進行尋址和操作。雖然物理磁盤尋址是按照扇區級進行的,但是內核執行的所有磁盤操作都是按照塊進行的。為了便於文件系統管理,塊的大小一般是扇區的整數倍,並且小於等於頁的大小。
3.當一個塊被調入內存時,它要存儲在一個緩沖區中。每一個緩沖區與一個塊對應,它相當於是磁盤塊在內存中的表示。每個緩沖區都有一個對應的描述符,用buffer_head結構體表示,稱作緩沖區頭,包含了內和操作緩沖區所需要的全部信息。
4.bio結構體表示了一次I/O操作所涉及到的所有內存頁。通過用片段來描述緩沖區,即使一個緩沖區分散在內存的多個位置上,bio結構體也能對內核保證I/O操作的執行。
5.bio中對應的是內存中的一個個頁,而緩沖區頭對應的是磁盤中的一個塊。
6.塊設備將它們掛起的塊I/O請求保存在請求隊列中,該隊列由request_queue結構體表示。請求隊列表中的每一項都是一個單獨的請求,由reques結構體表示。因為一個請求可能要操作多個連續的磁盤塊,所有每個請求可有由多個bio結構體組成。
7.雖然磁盤上的塊必須連續,但是在內存中的這些塊並不一定要連續。
8.I/O調度程序的工作是管理塊設備的請求隊列。通過合並與排序減少磁盤尋址時間。
9.為了保證磁盤尋址的效率,一般會盡量讓磁頭向一個方向移動,等到頭了再反過來移動,這樣可以縮短所有請求的磁盤尋址總時間,I/O調度程序稱作電梯調度。
10.2.6內核中內置了4種I/O調度: 預測(as)、完全公正排隊(cfq)、最終期限(deadline)、空操作(noop)。通過命令行選項 elevator=xxx 來啟用其中的任何一種。
2017-2018-1 20179202《Linux內核原理與分析》第八周作業