diff --git a/.gdbhelpers b/.gdbhelpers new file mode 100644 index 00000000..ae6daa00 --- /dev/null +++ b/.gdbhelpers @@ -0,0 +1,20 @@ +define px + printf "edi: 0x%x\nesi: 0x%x\nebp: 0x%x\nesp: 0x%x\nebx: 0x%x\nedx: 0x%x\necx: 0x%x\neax: 0x%x\neip: 0x%x\n", \ + $arg0->edi, $arg0->esi, $arg0->ebp, $arg0->esp, \ + $arg0->ebx, $arg0->edx, $arg0->ecx, $arg0->eax, $arg0->eip +end +document px + Pretty-print all fields of a task_context_t in hex. +end + +define stackdump + set $i = 0 + set $sp = $esp + while $i < $arg0 + printf "[%2d] 0x%08x\n", $i, *($sp + $i*4) + set $i = $i + 1 + end +end +document stackdump + Pretty-print N dwords starting at $esp. Usage: stackdump 16 +end diff --git a/CMakeLists.txt b/CMakeLists.txt index c90fd3af..8b40e390 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -237,6 +237,7 @@ add_custom_target( COMMAND echo "add-symbol-file ${CMAKE_BINARY_DIR}/mentos/kernel.bin" > ${CMAKE_BINARY_DIR}/gdb.run COMMAND echo "add-symbol-file ${CMAKE_BINARY_DIR}/mentos/bootloader.bin" >> ${CMAKE_BINARY_DIR}/gdb.run COMMAND find ${CMAKE_SOURCE_DIR}/files/bin -type f | xargs realpath | sed 's/^/add-symbol-file /' >> ${CMAKE_BINARY_DIR}/gdb.run + COMMAND echo "source ${CMAKE_SOURCE_DIR}/.gdbhelpers" >> ${CMAKE_BINARY_DIR}/gdb.run COMMAND echo "break boot.c: boot_main" >> ${CMAKE_BINARY_DIR}/gdb.run COMMAND echo "break kernel.c: kmain" >> ${CMAKE_BINARY_DIR}/gdb.run # Create the GDB connection file. diff --git a/libc/inc/list_head.h b/libc/inc/list_head.h index af8012b0..963d8afa 100644 --- a/libc/inc/list_head.h +++ b/libc/inc/list_head.h @@ -29,7 +29,7 @@ typedef struct list_head { /// @param pos the name of the iterator used to visit the list. /// @param store another list iterator to use as temporary storage. /// @param head the head for your list. -#define list_for_each_safe(pos, store, head) \ +#define list_for_each_safe(pos, store, head) \ for ((pos) = (head)->next, (store) = (pos)->next; (pos) != (head); (pos) = (store), (store) = (pos)->next) /// @brief Iterates over a list, but declares the iterator. @@ -41,8 +41,8 @@ typedef struct list_head { /// @param pos the name of the iterator used to visit the list. /// @param store another list iterator to use as temporary storage. /// @param head the head for your list. -#define list_for_each_safe_decl(pos, store, head) \ - for (list_head_t * (pos) = (head)->next, *(store) = (pos)->next; (pos) != (head); \ +#define list_for_each_safe_decl(pos, store, head) \ + for (list_head_t * (pos) = (head)->next, *(store) = (pos)->next; (pos) != (head); \ (pos) = (store), (store) = (pos)->next) /// @brief Iterates over a list backwards. @@ -53,17 +53,71 @@ typedef struct list_head { /// @brief Iterates over a list backwards, but declares the iterator. /// @param pos the name of the iterator used to visit the list. /// @param head the head for your list. -#define list_for_each_prev_decl(pos, head) \ +#define list_for_each_prev_decl(pos, head) \ for (list_head_t * (pos) = (head)->prev; (pos) != (head); (pos) = (pos)->prev) +/// @brief Resets iteration when current node might have been removed. +/// @param pos The current iterator. +/// @param store The temporary storage used in the loop. +#define list_safe_reset_next(pos, store) \ + (store) = (pos)->next + +/// @brief Get the next entry in the list. +/// @param pos the current entry. +/// @param member the name of the list_head within the struct. +/// @return the next entry in the list. +#define list_next_entry(pos, member) \ + list_entry((pos)->member.next, typeof(*(pos)), member) + +/// @brief Get the previous entry in the list. +/// @param pos the current entry. +/// @param member the name of the list_head within the struct. +/// @return the previous entry in the list. +#define list_prev_entry(pos, member) \ + list_entry((pos)->member.prev, typeof(*(pos)), member) + +/// @brief Get the first entry in the list. +/// @param head the head of the list. +/// @param type the type of the struct this is embedded in. +/// @param member the name of the list_head within the struct. +/// @return the first entry in the list. +#define list_first_entry(head, type, member) \ + list_entry((head)->next, type, member) + +/// @brief Get the last entry in the list. +/// @param head the head of the list. +/// @param type the type of the struct this is embedded in. +/// @param member the name of the list_head within the struct. +/// @return the last entry in the list. +#define list_last_entry(head, type, member) \ + list_entry((head)->prev, type, member) + /// @brief Ensures that the given list is valid. /// @param list the list to validate. -static inline void list_head_validate(const list_head_t *list) -{ - assert(list && "List is NULL."); - assert(list->prev && "List->prev is NULL."); - assert(list->next && "List->next is NULL."); -} +#define list_head_validate(list) \ + (assert((list) != NULL && "List is NULL."), \ + assert((list)->next && "List next is NULL."), \ + assert((list)->prev && "List prev is NULL.")) + +/// @brief Tests whether the given list is empty. +/// @param head The list to check. +/// @return 1 if empty, 0 otherwise. +#define list_head_empty(list) \ + (list_head_validate(list), (list)->next == (list)) + +/// @brief Tests whether the given list is singular. +/// @param list The list to check. +/// @return 1 if singular, 0 otherwise. +#define list_head_is_singular(list) \ + (list_head_validate(list), (list)->next == (list)->prev) + +/// @brief Checks if an entry is the head of the list. +/// @param pos The entry to check. +/// @param head The list head. +/// @param member The member name inside the struct. +/// @return 1 if the entry is the head, 0 otherwise. +#define list_head_entry_is_head(pos, head, member) \ + (&(pos)->member == (head)) /// @brief Initializes the list_head_t. /// @param head The head of your list. @@ -73,15 +127,6 @@ static inline void list_head_init(list_head_t *head) head->next = head->prev = head; } -/// @brief Tests whether the given list is empty. -/// @param head The list to check. -/// @return 1 if empty, 0 otherwise. -static inline int list_head_empty(const list_head_t *head) -{ - list_head_validate(head); - return head->next == head; -} - /// @brief Computes the size of the list. /// @param head The head of the list. /// @return the size of the list. @@ -232,3 +277,43 @@ static inline void list_head_swap(list_head_t *entry1, list_head_t *entry2) } list_head_insert_after(entry1, pos); } + +/// @brief Moves the entry to the new head. +/// @param entry the entry we want to move. +/// @param new_head the new head where we want to move the entry. +static inline void list_head_move(list_head_t *entry, list_head_t *new_head) +{ + list_head_remove(entry); + list_head_insert_after(entry, new_head); +} + +/// @brief Moves the entry to the new head, but places it at the end of the list. +/// @param entry the entry we want to move. +/// @param new_head the new head where we want to move the entry. +static inline void list_head_move_tail(list_head_t *entry, list_head_t *new_head) +{ + list_head_remove(entry); + list_head_insert_before(entry, new_head); +} + +/// @brief Moves the entry to the new head, but places it at the end of the list. +/// @param list the list we want to move. +/// @param head the new head where we want to move the list. +static inline void list_head_splice(list_head_t *list, list_head_t *head) +{ + if (!list_head_empty(list)) { + list->next->prev = head; + list->prev->next = head->next; + head->next->prev = list->prev; + head->next = list->next; + } +} + +/// @brief Moves the list to the new head. +/// @param list the list we want to move. +/// @param head the new head where we want to move the list. +static inline void list_head_splice_init(list_head_t *list, list_head_t *head) +{ + list_head_splice(list, head); + list_head_init(list); +} diff --git a/mentos/CMakeLists.txt b/mentos/CMakeLists.txt index 1315afce..1c404061 100644 --- a/mentos/CMakeLists.txt +++ b/mentos/CMakeLists.txt @@ -109,6 +109,7 @@ add_library( ${CMAKE_SOURCE_DIR}/mentos/src/process/process.c ${CMAKE_SOURCE_DIR}/mentos/src/process/wait.c ${CMAKE_SOURCE_DIR}/mentos/src/process/user.S + ${CMAKE_SOURCE_DIR}/mentos/src/process/switch.S ${CMAKE_SOURCE_DIR}/mentos/src/sys/utsname.c ${CMAKE_SOURCE_DIR}/mentos/src/sys/module.c ${CMAKE_SOURCE_DIR}/mentos/src/system/errno.c diff --git a/mentos/inc/process/process.h b/mentos/inc/process/process.h index 4e760034..6ab16aa0 100644 --- a/mentos/inc/process/process.h +++ b/mentos/inc/process/process.h @@ -18,6 +18,11 @@ /// The default dimension of the stack of a process (1 MByte). #define DEFAULT_STACK_SIZE (1 * M) +struct context { + uint32_t edi, esi, ebp, esp, ebx, edx, ecx, eax; // General-purpose registers + uint32_t eip; // Instruction pointer +}; + /// @brief This structure is used to track the statistics of a process. /// @details /// While the other variables also play a role in @@ -158,6 +163,8 @@ typedef struct task_struct { /// Buffer for managing inputs from keyboard. rb_keybuffer_t keyboard_rb; + struct context ctx; + //==== Future work ========================================================= // - task's attributes: // struct task_struct __rcu *real_parent; @@ -182,3 +189,5 @@ int process_create_init(const char *path); /// descriptor or NULL if the file descriptor is invalid or the file has been /// closed. vfs_file_descriptor_t *fget(int fd); + +void test_kernel_context_switch(void); diff --git a/mentos/src/kernel.c b/mentos/src/kernel.c index 234ce712..c2d8aac3 100644 --- a/mentos/src/kernel.c +++ b/mentos/src/kernel.c @@ -461,6 +461,8 @@ int kmain(boot_info_t *boot_informations) } print_ok(); + test_kernel_context_switch(); + // We have completed the booting procedure. pr_notice("Booting done, jumping into init process.\n"); // Switch to the page directory of init. diff --git a/mentos/src/process/process.c b/mentos/src/process/process.c index 93b1fe85..af0432da 100644 --- a/mentos/src/process/process.c +++ b/mentos/src/process/process.c @@ -511,7 +511,7 @@ pid_t sys_fork(pt_regs_t *f) task_struct *proc = __alloc_task(current, current, current->name); // Copy the father's stack, memory, heap etc... to the child process proc->mm = mm_clone(current->mm); - // Set the eax as 0, to indicate the child process + // Set the eax as 0, to indicate the child process. proc->thread.regs.eax = 0; // Enable the interrupts. proc->thread.regs.eflags = proc->thread.regs.eflags | EFLAG_IF; @@ -695,3 +695,169 @@ int sys_execve(pt_regs_t *f) pr_debug("Executing '%s' (pid: %d)...\n", current->name, current->pid); return 0; } + +// === KERNEL CONTEXT SWITCH TEST ================================================== +// This is a test for the kernel context switch. It is not used in the system. + +#define STACK_SIZE 4096 + +typedef struct { + uint32_t edi; // +0 + uint32_t esi; // +4 + uint32_t ebp; // +8 + uint32_t esp; // +12 + uint32_t ebx; // +16 + uint32_t edx; // +20 + uint32_t ecx; // +24 + uint32_t eax; // +28 + uint32_t eip; // +32 +} task_context_t; + +typedef struct task { + const char *name; + unsigned int pid; + task_context_t context; + uint8_t stack[STACK_SIZE]; + list_head_t list; +} task_t; + +extern void context_switch(task_context_t *old, task_context_t *new); + +void task_schedule(void); + +static list_head_t task_list; +static task_t *current_task = NULL; + +void full_dump_task_information(const task_t *task) +{ + const task_context_t *ctx = &task->context; + pr_info("Task (%s) context (CTX: %p):\n", task->name, ctx); + pr_info(" EAX: 0x%08x, EBX: 0x%08x, ECX: 0x%08x, EDX: 0x%08x\n", ctx->eax, ctx->ebx, ctx->ecx, ctx->edx); + pr_info(" ESI: 0x%08x, EDI: 0x%08x, EIP: 0x%08x\n", ctx->esi, ctx->edi, ctx->eip); + pr_info(" EBP: 0x%08x, ESP: 0x%08x\n", ctx->ebp, ctx->esp); + for (unsigned i = 0; i < 10; ++i) { + uintptr_t address = (uintptr_t)(ctx->esp) + (i * sizeof(uintptr_t)); + uintptr_t value = *(uintptr_t *)address; + pr_info(" [%2d] 0x%08x: 0x%08x\n", i, address, value); + } +} + +void full_dump_task_list(void) +{ + pr_info("Task list (%p):\n", &task_list); + if (list_head_is_singular(&task_list)) { + pr_info(" No tasks in the system.\n"); + return; + } + list_for_each_decl (it, &task_list) { + task_t *task = list_entry(it, task_t, list); + full_dump_task_information(task); + } +} + +void task_body(void) +{ + assert(current_task && "No current task set."); + + int count = current_task->pid; + + pr_notice(">>> ----------------------------------\n"); + pr_notice(">>> Task `%s` entered (pid:%4d, count: %4d).\n", current_task->name, current_task->pid, count); + while (1) { + pr_notice(">>> Task `%s` entered the while loop (pid:%4d, count: %4d).\n", current_task->name, current_task->pid, count); + task_schedule(); + pr_notice(">>> Task `%s` came back from task_schedule and resuming (pid:%4d, count: %4d).\n", current_task->name, current_task->pid, count); + task_schedule(); + count++; + } + __builtin_unreachable(); +} + +void task_schedule(void) +{ + pr_info(">>> \n"); + pr_info(">>> =======================================================================================\n"); + + assert(current_task && "No current task set."); + task_t *prev = current_task; + task_t *next = NULL; + + if (list_head_is_singular(&task_list)) { + pr_warning("Only one task in system; cannot schedule.\n"); + return; + } + + if (list_next_entry(prev, list) == list_entry(&task_list, task_t, list)) { + next = list_first_entry(&task_list, task_t, list); + } else { + next = list_next_entry(prev, list); + } + + current_task = next; + + pr_info(">>> Switching from task '%s' to '%s'\n", prev->name, next->name); + full_dump_task_information(prev); + full_dump_task_information(next); + context_switch(&prev->context, &next->context); + + __builtin_unreachable(); +} + +void init_task(task_t *task, const char *name, void (*entry)(void)) +{ + static unsigned int pid = 1; + + memset(task, 0, sizeof(*task)); + // Set the name of the task. + task->name = name; + // Set the PID of the task. + task->pid = pid++; + // Prepare the stack. + uintptr_t *stack_top = (uintptr_t *)(task->stack + STACK_SIZE); + *(--stack_top) = (uintptr_t)entry; // EIP + *(--stack_top) = 0; // EDI + *(--stack_top) = 0; // ESI + *(--stack_top) = 0; // EBP + *(--stack_top) = 0; // Dummy ESP + *(--stack_top) = 0; // EBX + *(--stack_top) = 0; // EDX + *(--stack_top) = 0; // ECX + *(--stack_top) = 0; // EAX + task->context.esp = (uintptr_t)stack_top; + list_head_init(&task->list); + list_head_insert_before(&task->list, &task_list); +} + +void test_kernel_context_switch(void) +{ + pr_notice("Setting up kernel context switch test\n"); + + list_head_init(&task_list); + + task_t task_a; + task_t task_b; + task_t task_c; + + init_task(&task_a, "task_a", task_body); + init_task(&task_b, "task_b", task_body); + init_task(&task_c, "task_c", task_body); + + pr_notice("task_body = 0x%08x\n", (uintptr_t)&task_body); + + full_dump_task_information(&task_a); + full_dump_task_information(&task_b); + full_dump_task_information(&task_c); + + full_dump_task_list(); + + pr_notice("Switching from BOOT to task A...\n"); + + // Set the current task to task A. + current_task = &task_a; + // Set the context of task A. + context_switch(NULL, &task_a.context); + + pr_crit("Returned from context_switch unexpectedly.\n"); + + __builtin_unreachable(); +} \ No newline at end of file diff --git a/mentos/src/process/scheduler.c b/mentos/src/process/scheduler.c index b00761e5..3a655539 100644 --- a/mentos/src/process/scheduler.c +++ b/mentos/src/process/scheduler.c @@ -28,6 +28,8 @@ /// @param stack The stack to use. extern void enter_userspace(uintptr_t location, uintptr_t stack); +extern void context_switch(struct context *old, struct context *new); + /// The list of processes. runqueue_t runqueue; @@ -351,10 +353,10 @@ int sys_setpgid(pid_t pid, pid_t pgid) } /// Returns the attributes of the runnign process. -#define RETURN_PROCESS_ATTR_OR_EPERM(attr) \ - if (runqueue.curr) { \ - return runqueue.curr->attr; \ - } \ +#define RETURN_PROCESS_ATTR_OR_EPERM(attr) \ + if (runqueue.curr) { \ + return runqueue.curr->attr; \ + } \ return -EPERM; uid_t sys_getuid(void) { RETURN_PROCESS_ATTR_OR_EPERM(ruid); } @@ -364,38 +366,38 @@ gid_t sys_getgid(void) { RETURN_PROCESS_ATTR_OR_EPERM(rgid); } gid_t sys_getegid(void) { RETURN_PROCESS_ATTR_OR_EPERM(gid); } /// Checks the given ID. -#define FAIL_ON_INV_ID(id) \ - if ((id) < 0) { \ - return -EINVAL; \ +#define FAIL_ON_INV_ID(id) \ + if ((id) < 0) { \ + return -EINVAL; \ } /// Checks the ID, and if there is a running process. -#define FAIL_ON_INV_ID_OR_PROC(id) \ - FAIL_ON_INV_ID(id) \ - if (!runqueue.curr) { \ - return -EPERM; \ +#define FAIL_ON_INV_ID_OR_PROC(id) \ + FAIL_ON_INV_ID(id) \ + if (!runqueue.curr) { \ + return -EPERM; \ } /// If the process is ROOT, set the attribute and return 0. -#define IF_PRIVILEGED_SET_ALL_AND_RETURN(attr) \ - if (runqueue.curr->uid == 0) { \ - runqueue.curr->r##attr = runqueue.curr->attr = attr; \ - return 0; \ +#define IF_PRIVILEGED_SET_ALL_AND_RETURN(attr) \ + if (runqueue.curr->uid == 0) { \ + runqueue.curr->r##attr = runqueue.curr->attr = attr; \ + return 0; \ } /// Checks the attributes, resets them, and returns 0. -#define IF_RESET_SET_AND_RETURN(attr) \ - if (runqueue.curr->r##attr == (attr)) { \ - runqueue.curr->attr = attr; \ - return 0; \ +#define IF_RESET_SET_AND_RETURN(attr) \ + if (runqueue.curr->r##attr == (attr)) { \ + runqueue.curr->attr = attr; \ + return 0; \ } /// If the process is ROOT set the attribute, otherwise return failure. -#define SET_IF_PRIVILEGED_OR_FAIL(attr) \ - if (runqueue.curr->uid == 0) { \ - runqueue.curr->attr = attr; \ - } else { \ - return -EPERM; \ +#define SET_IF_PRIVILEGED_OR_FAIL(attr) \ + if (runqueue.curr->uid == 0) { \ + runqueue.curr->attr = attr; \ + } else { \ + return -EPERM; \ } int sys_setuid(uid_t uid) diff --git a/mentos/src/process/switch.S b/mentos/src/process/switch.S new file mode 100644 index 00000000..d063f243 --- /dev/null +++ b/mentos/src/process/switch.S @@ -0,0 +1,94 @@ +; MentOS, The Mentoring Operating system project +; @file switch.S +; @brief This file contains the context switch code. +; @copyright (c) 2014-2021 This file is distributed under the MIT License. +; See LICENSE.md for details. + +; ----------------------------------------------------------------------------- +; SECTION (text) +; ----------------------------------------------------------------------------- +section .text + +global context_switch + +; void context_switch(struct context *old, struct context *new); +; +; @brief Perform a low-level context switch between two task contexts. +; @param old Pointer to the current task's context to save. +; @param new Pointer to the next task's context to restore. +; +; This function saves the current register state of the calling task into `old` +; and restores the register state from `new`, effectively switching execution +; to another task. +; +; At entry, the stack contains the return address and arguments: +; [esp + 0] → return address +; [esp + 4] → old (task_context_t*) +; [esp + 8] → new (task_context_t*) +; +; However, since `pusha` is used at the start of this function to save all +; general-purpose registers, the stack pointer `esp` moves down by 32 bytes +; (8 registers * 4 bytes each). To access the original arguments again, +; we use: +; lea edx, [esp + 32] // edx now points to the caller's original esp +; mov eax, [edx + 32 + 4] // old +; mov ebx, [edx + 32 + 8] // new +; +; This technique ensures compatibility with the cdecl calling convention +; and avoids corrupting the stack layout while preserving the register state. +; +context_switch: + ; Save the ESP before pusha. + lea edx, [esp + 32] + ; Push the whole context. + pusha + ; Get the current context. + mov eax, [esp + 32 + 4] ; old + ; Check if the context is NULL. + test eax, eax + jz .skip_save + + ; Store to struct in the same order as they appear on stack. + mov ecx, [esp + 32] ; old.eip + mov [eax + 32], ecx + + mov ecx, [esp + 28] ; old.edi + mov [eax + 0], ecx + + mov ecx, [esp + 24] ; old.esi + mov [eax + 4], ecx + + mov ecx, [esp + 20] ; old.ebp + mov [eax + 8], ecx + + mov [eax + 12], edx ; old.esp (saved before pusha) + + mov ecx, [esp + 12] ; old.ebx + mov [eax + 16], ecx + + mov ecx, [esp + 8] ; old.edx + mov [eax + 20], ecx + + mov ecx, [esp + 4] ; old.ecx + mov [eax + 24], ecx + + mov ecx, [esp + 0] ; old.eax + mov [eax + 28], ecx + +.skip_save: + ; Get the pointer to the new context. + mov eax, [esp + 32 + 8] ; new + ; Restore the context. + mov esp, [eax + 12] ; new.esp + ; jump to the return address. + jmp .resume_execution + +.resume_execution: + popa + pop ebx + jmp ebx + +; ----------------------------------------------------------------------------- +; SECTION (note) - Inform the linker that the stack does not need to be executable +; ----------------------------------------------------------------------------- +section .note.GNU-stack diff --git a/programs/tests/CMakeLists.txt b/programs/tests/CMakeLists.txt index 118ea5c2..e6ee633d 100644 --- a/programs/tests/CMakeLists.txt +++ b/programs/tests/CMakeLists.txt @@ -45,6 +45,7 @@ set(TEST_LIST t_list.c t_hashmap.c t_scanf.c + t_fgets.c ) # Set the directory where the compiled binaries will be placed. diff --git a/programs/tests/t_fgets.c b/programs/tests/t_fgets.c new file mode 100644 index 00000000..0b32e0d7 --- /dev/null +++ b/programs/tests/t_fgets.c @@ -0,0 +1,28 @@ +/// @file t_fgets.c +/// @brief Test the fgets function with STDIN_FILENO. +/// @copyright (c) 2024 This file is distributed under the MIT License. +/// See LICENSE.md for details. + +#include +#include +#include + +int main(void) +{ + char buffer[128]; + + printf("Enter a line of text (including spaces): "); + if (fgets(buffer, sizeof(buffer), STDIN_FILENO) == NULL) { + printf("Failed to read line.\n"); + return 1; + } + + // Remove trailing newline if present + size_t len = strlen(buffer); + if (len > 0 && buffer[len - 1] == '\n') { + buffer[len - 1] = '\0'; + } + + printf("You entered: \"%s\"\n", buffer); + return 0; +}