目錄
建立抽象
實現加載
實現sys_execv
!!!提示:因為實現問題沒有測試。所以更像是筆記!
exec
函數的作用是用新的可執行文件替換當前進程的程序體。具體來說,exec
會將當前正在運行的用戶進程的進程體(包括代碼段、數據段、堆、棧等)替換為一個新的可執行文件的進程體。這樣,新的程序會接管當前進程的地址空間,繼續執行新程序的代碼,但該進程的 PID(進程ID)保持不變。也就是說,執行 exec
后,原來進程的地址空間被清除,并且新程序的內容會加載到同樣的進程中,繼續執行。
為什么需要實現 exec
呢?這個問題的答案與 shell 的工作方式密切相關。在實現一些簡單的命令時,我們使用了類似 if-else if
的結構來判斷并執行不同的命令。然而,這種方法存在很大的局限性。首先,它無法處理用戶輸入的新命令,因為我們不能預見到用戶會輸入什么命令,且每添加一個新命令就需要修改代碼并重新編譯。這種方式不僅繁瑣,而且無法應對外部程序的運行。
exec
的實現解決了這個問題。當 exec
被調用時,它允許用戶運行外部程序,而不需要修改 shell 本身的代碼。用戶輸入的命令會被解析,且通過 exec
函數加載并執行對應的外部程序,從而提供了更靈活的命令執行方式。
exec
是一個函數簇,包含多個相關的函數,區別主要在于如何表示程序對象以及是否傳入環境變量。例如,execv
函數就不需要傳入環境變量,但其他 exec
函數可能會接受額外的環境變量。
當調用 execv
時,如果執行成功,進程將直接跳轉到新程序,并不會返回,因此它沒有返回值。調用 execv
失敗時,它會返回 -1
,并設置錯誤碼。這是因為 exec
執行新程序時,原進程的執行流被完全替換,進程不會再回到原來的位置,因而不需要像傳統函數那樣返回值。
建立抽象
我們先對exe文件做抽象:
extern void intr_exit(void);
typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
typedef uint16_t Elf32_Half;
?
/* 32-bit ELF header */
struct Elf32_Ehdr {unsigned char e_ident[16]; // ELF identification bytesElf32_Half e_type; ? ? ? ? // Type of file (e.g., executable)Elf32_Half e_machine; ? ? ?// Machine architectureElf32_Word e_version; ? ? ?// ELF versionElf32_Addr e_entry; ? ? ? ?// Entry point addressElf32_Off e_phoff; ? ? ? ? // Program header offsetElf32_Off e_shoff; ? ? ? ? // Section header offsetElf32_Word e_flags; ? ? ? ?// Processor-specific flagsElf32_Half e_ehsize; ? ? ? // ELF header sizeElf32_Half e_phentsize; ? ?// Program header entry sizeElf32_Half e_phnum; ? ? ? ?// Number of program headersElf32_Half e_shentsize; ? ?// Section header entry sizeElf32_Half e_shnum; ? ? ? ?// Number of section headersElf32_Half e_shstrndx; ? ? // Section header string table index
};
?
/* Program header (segment descriptor) */
struct Elf32_Phdr {Elf32_Word p_type; ? // Segment type (e.g., PT_LOAD)Elf32_Off p_offset; ?// Offset in fileElf32_Addr p_vaddr; ?// Virtual address in memoryElf32_Addr p_paddr; ?// Physical address (unused)Elf32_Word p_filesz; // Size of segment in fileElf32_Word p_memsz; ?// Size of segment in memoryElf32_Word p_flags; ?// Segment flagsElf32_Word p_align; ?// Segment alignment
};
?
/* Segment types */
enum segment_type {PT_NULL, ? ?// Ignore segmentPT_LOAD, ? ?// Loadable segmentPT_DYNAMIC, // Dynamic loading informationPT_INTERP, ?// Name of dynamic loaderPT_NOTE, ? ?// Auxiliary informationPT_SHLIB, ? // ReservedPT_PHDR ? ? // Program header
};
這段代碼定義了32位ELF(Executable and Linkable Format)格式的結構體以及相關的常量,用于描述ELF文件的頭部和程序段的描述。具體來說,主要包括以下內容:
-
Elf32_Ehdr: 該結構體表示ELF文件的頭部,包含了ELF文件的基本信息,如文件標識、類型、機器架構、入口地址、程序頭的偏移量等。具體字段的含義如下:
-
e_ident
:ELF文件標識字節,用于標識文件類型和版本。 -
e_type
:文件類型,表明ELF文件是可執行文件、共享庫文件還是其他類型。 -
e_machine
:表示機器架構的字段,如x86、ARM等。 -
e_version
:ELF版本,通常為1。 -
e_entry
:程序入口點的地址。 -
e_phoff
:程序頭部的偏移量,指向包含程序段信息的位置。 -
e_shoff
:節頭部的偏移量,指向包含節信息的位置。 -
e_flags
:處理器特定的標志。 -
e_ehsize
:ELF頭部的大小。 -
e_phentsize
:程序頭項的大小。 -
e_phnum
:程序頭的數量。 -
e_shentsize
:節頭項的大小。 -
e_shnum
:節頭的數量。 -
e_shstrndx
:節頭字符串表的索引。
-
-
Elf32_Phdr: 該結構體表示ELF文件中的程序頭(segment descriptor),用于描述文件中的每個段。字段的含義如下:
-
p_type
:段的類型,如可加載段、動態段等。 -
p_offset
:段在文件中的偏移。 -
p_vaddr
:段在內存中的虛擬地址。 -
p_paddr
:段在物理內存中的地址(通常不使用)。 -
p_filesz
:段在文件中的大小。 -
p_memsz
:段在內存中的大小。 -
p_flags
:段的標志,如可讀、可寫、可執行等。 -
p_align
:段的對齊方式。
-
-
segment_type:該枚舉定義了常見的段類型,如:
-
PT_NULL
:表示忽略該段。 -
PT_LOAD
:表示可加載的段(常見的代碼和數據段)。 -
PT_DYNAMIC
:動態加載信息。 -
PT_INTERP
:動態加載器的名稱。 -
PT_NOTE
:輔助信息。 -
PT_SHLIB
:保留段。 -
PT_PHDR
:程序頭。
-
實現加載
/* Load a segment from a file into virtual memory at the specified address */
static bool segment_load(int32_t fd, uint32_t offset, uint32_t filesz,uint32_t vaddr) {uint32_t vaddr_first_page =vaddr & 0xfffff000; // First page of the virtual addressuint32_t size_in_first_page =PG_SIZE - (vaddr & 0x00000fff); // Size of the segment in the first pageuint32_t occupy_pages = 0;
?// If the segment doesn't fit in a single pageif (filesz > size_in_first_page) {uint32_t left_size = filesz - size_in_first_page;occupy_pages = ROUNDUP(left_size, PG_SIZE) + 1; // +1 for the first page} else {occupy_pages = 1;}
?// Allocate memory for the segment in the process's address spaceuint32_t page_idx = 0;uint32_t vaddr_page = vaddr_first_page;while (page_idx < occupy_pages) {uint32_t *pde = pde_ptr(vaddr_page); // Page directory entryuint32_t *pte = pte_ptr(vaddr_page); // Page table entry
?// Allocate memory if PDE or PTE doesn't existif (!(*pde & PG_P_1) || !(*pte & PG_P_1)) {if (!get_a_page(PF_USER, vaddr_page)) {return false;}}vaddr_page += PG_SIZE;page_idx++;}
?// Read the segment data from the file and load it into memorysys_lseek(fd, offset, SEEK_SET);sys_read(fd, (void *)vaddr, filesz);return true;
}
?
函數 segment_load
負責將一個可執行文件中的特定段加載到進程的虛擬內存中,它接收四個參數:文件描述符 fd
,段在文件中的偏移量 offset
,段大小 filesz
,以及段應加載到的虛擬地址 vaddr
。其中 filesz
命名雖然讓人容易聯想到整個文件大小,但它其實是 ELF 格式中段頭部的字段名 p_filesz
,表示當前這個段在文件中的實際大小,因此用作參數名是為了與 ELF 中的結構保持一致。
段的加載實質上就是內核為新進程分配內存的過程。由于程序通常由多個段組成,內核需要對每個段逐一加載。加載時以頁為單位進行內存管理,因此即使一個段不滿一頁,也必須以頁為粒度分配內存。變量 vaddr_first_page
是將段的虛擬地址 vaddr
向下對齊到頁起始地址,用于確定從哪里開始分配頁框。而變量 size_in_first_page
則表示該段在第一頁中所占用的字節數,如果 filesz
大于這個值,說明段會跨頁,因此接下來計算還需多少頁框,最終由 occupy_pages
給出總的頁框數。
接下來是頁框分配邏輯,考慮到這是 exec 執行新程序的場景,當前進程的頁表結構還在用,若某虛擬地址已經存在對應的物理頁,則無需重新分配,只需直接復用原頁框覆蓋其內容即可;否則就通過 get_a_page
分配一個新頁框。分配時逐頁判斷并處理,直到整段的地址空間都被準備好。
頁框分配完成后,便可以真正加載段的數據了。首先使用 sys_lseek
將文件讀指針移動到段的起始偏移位置 offset
,再用 sys_read
將長度為 filesz
的數據讀入到從 vaddr
開始的虛擬地址中。至此,這個段被完整加載進內存。整個過程體現了分段加載、按頁管理、懶分配頁框的設計思路,也保證了內存使用的靈活性與效率。
/* Load a user program from the filesystem by pathname, return entry point* address or -1 on failure */
static int32_t load(const char *pathname) {int32_t ret = -1;struct Elf32_Ehdr elf_header;struct Elf32_Phdr prog_header;k_memset(&elf_header, 0, sizeof(struct Elf32_Ehdr));
?int32_t fd = sys_open(pathname, O_RDONLY); // Open the program fileif (fd == -1) {return -1;}
?// Read the ELF header from the fileif (sys_read(fd, &elf_header, sizeof(struct Elf32_Ehdr)) !=sizeof(struct Elf32_Ehdr)) {ret = -1;goto done;}
?// Verify the ELF headerif (k_memcmp(elf_header.e_ident, "\177ELF\1\1\1", 7) ||elf_header.e_type != 2 || elf_header.e_machine != 3 ||elf_header.e_version != 1 || elf_header.e_phnum > 1024 ||elf_header.e_phentsize != sizeof(struct Elf32_Phdr)) {ret = -1;goto done;}
?Elf32_Off prog_header_offset = elf_header.e_phoff;Elf32_Half prog_header_size = elf_header.e_phentsize;
?// Iterate over all program headersuint32_t prog_idx = 0;while (prog_idx < elf_header.e_phnum) {k_memset(&prog_header, 0, prog_header_size);
?// Seek to the program header location in the filesys_lseek(fd, prog_header_offset, SEEK_SET);
?// Read the program header from the fileif (sys_read(fd, &prog_header, prog_header_size) != prog_header_size) {ret = -1;goto done;}
?// If the segment is loadable, load it into memoryif (PT_LOAD == prog_header.p_type) {if (!segment_load(fd, prog_header.p_offset, prog_header.p_filesz,prog_header.p_vaddr)) {ret = -1;goto done;}}
?// Move to the next program headerprog_header_offset += elf_header.e_phentsize;prog_idx++;}
?ret = elf_header.e_entry; // Return the entry point of the program
done:sys_close(fd); // Close the filereturn ret;
}
函數 load
的核心功能是加載一個 ELF 格式的用戶程序文件,并將其段映射到當前進程的虛擬地址空間中。如果加載成功,返回值是該程序的入口地址(即進程執行的起點);如果失敗,返回 ?1。
函數開始先聲明兩個結構體變量:elf_header
和 prog_header
,分別用于保存 ELF 文件頭和程序段頭。在讀取 ELF 文件頭后(第 102 行),程序緊接著從第 108 行開始驗證 ELF 文件是否合法。
首先檢查的是 ELF 文件的魔數 e_ident[0-6]
,這 7 個字節應依次為:
-
0x7F
(用八進制\177
表示) -
'E'
(0x45) -
'L'
(0x4C) -
'F'
(0x46) -
1
:32 位格式 -
1
:小端格式 -
1
:版本號
這幾項是 ELF 文件的標準標志,如果不匹配,說明該文件不是合法的 ELF 可執行文件。接下來還會檢查以下幾個字段:
-
e_type
是否為ET_EXEC
(值為 2,代表可執行文件) -
e_machine
是否為EM_386
(值為 3,表示 x86 架構) -
e_version
是否為 1(當前 ELF 版本) -
e_phnum
(程序頭數量)是否小于等于 1024 -
e_phentsize
(每個程序頭條目的大小)是否等于sizeof(Elf32_Phdr)
這些檢查都通過后,才認為這是一個有效的 ELF 可執行文件。
接下來,從 ELF 頭中讀取段頭信息的起始偏移地址 e_phoff
,讀取到變量 prog_header_offset
。段頭條目的字節大小 e_phentsize
賦給 prog_header_size
,條目總數 e_phnum
用于控制接下來的循環。
然后從第 122 行進入循環,逐個讀取每個段頭。每次循環會先通過 sys_lseek
將文件指針跳到對應段頭位置,然后通過 sys_read
讀取一條段頭到 prog_header
。第 136 行判斷該段是否是 PT_LOAD
類型,也就是是否是可加載段。如果是,就調用 segment_load
,將該段的內容從文件加載到內存對應的虛擬地址。
所有段處理完畢后,從 ELF 頭中提取程序入口地址 e_entry
賦給返回值 ret
,這表示程序開始執行的地址。
最后,無論是否加載成功,都會通過 sys_close
關閉打開的 ELF 文件,返回值為加載成功的入口地址或失敗的 ?1。
總體來說,load
函數的實現非常典型地體現了 ELF 格式的標準解析流程、段式加載方式、虛擬內存分配控制等關鍵內核概念,是內核啟動用戶進程的核心部分之一。
實現sys_execv
/* Replace the current process with the program at the specified path */
int32_t sys_execv(const char *path, const char *argv[]) {uint32_t argc = 0;while (argv[argc]) {argc++; // Count the number of arguments}
?// Load the program and get its entry pointint32_t entry_point = load(path);if (entry_point == -1) { // If loading failed, return -1return -1;}
?TaskStruct *cur = current_thread(); // Get the current running thread (process)k_memcpy(cur->name, path, TASK_NAME_ARRAY_SZ); // Update the process name
?// Update the stack with the argumentsInterrupt_Stack *intr_0_stack =(Interrupt_Stack *)((uint32_t)cur + PG_SIZE - sizeof(Interrupt_Stack));intr_0_stack->ebx = (int32_t)argv;intr_0_stack->ecx = argc;intr_0_stack->eip = (void *)entry_point;intr_0_stack->esp = (void *)KERNEL_V_START; // Set stack pointer to the highest// user space address
?// Jump to the entry point of the new processasm volatile("movl %0, %%esp; jmp intr_exit":: "g"(intr_0_stack): "memory");return 0;
}
sys_execv
函數的作用是將當前正在運行的進程替換為另一個可執行文件 path
所指定的程序,同時把參數數組 argv
一并傳給新程序。這個過程不會返回,一旦成功,當前進程就“變成”了另一個程序。
首先,函數會遍歷 argv
,統計參數個數并存入變量 argc
。接著調用 load(path)
試圖加載用戶程序,如果加載失敗(返回 -1),函數立即返回 -1。若加載成功,程序的入口地址會被保存下來,作為后續執行的跳轉目標。
之后,函數更新當前進程控制塊中的 name
字段,使其反映正在執行的新程序名,這樣在通過 ps 等工具查看時會顯示為新程序的名字。
然后獲取當前線程的內核棧頂地址。此時棧中存儲的是舊進程的中斷現場,但很快要把這些內容替換掉,準備啟動新進程。函數將參數個數 argc
寫入棧中保存的 ecx
寄存器位置,將參數數組 argv
的地址寫入 ebx
寄存器位置。因為 ebx
通常用于保存基地址,而 ecx
常用于計數,這是一種傳統習慣,也便于未來從運行庫中取參數。接著將程序入口地址寫入 eip
,用于后續跳轉執行;再將用戶棧指針 esp
初始化為 0xc0000000,即用戶空間最高地址,以便新程序使用。
設置完成后,通過內聯匯編將 esp 寄存器修改為新的內核棧地址,并跳轉到 intr_exit。這個跳轉操作會恢復棧中保存的所有寄存器狀態,包括 eip、esp 和參數寄存器等,相當于“偽裝”從中斷中返回,從而進入新程序的執行流程。
因為這個過程是不可逆的,調用成功后不會返回到原來的函數中,所以 return 0 這一行永遠不會執行,它的存在只是為了避免編譯器報錯。整段代碼實現的是典型的 exec 功能,用一個新的程序完全替換當前進程的執行內容。
下一篇
從0開始的操作系統手搓教程46——實現wait和exit-CSDN博客文章瀏覽閱讀522次,點贊7次,收藏8次。實現exit和wait(筆記,因為實現問題沒有測試)https://blog.csdn.net/charlie114514191/article/details/146144946