diff --git a/Documentation/hal-calling-convention.md b/Documentation/hal-calling-convention.md index 6df5017..b7bed26 100644 --- a/Documentation/hal-calling-convention.md +++ b/Documentation/hal-calling-convention.md @@ -109,14 +109,14 @@ void hal_context_restore(jmp_buf env, int32_t val); /* Restore context + process The ISR in `boot.c` performs a complete context save of all registers: ``` -Stack Frame Layout (144 bytes, 33 words × 4 bytes, offsets from sp): +Stack Frame Layout (144 bytes, 36 words × 4 bytes, offsets from sp): 0: ra, 4: gp, 8: tp, 12: t0, 16: t1, 20: t2 24: s0, 28: s1, 32: a0, 36: a1, 40: a2, 44: a3 48: a4, 52: a5, 56: a6, 60: a7, 64: s2, 68: s3 72: s4, 76: s5, 80: s6, 84: s7, 88: s8, 92: s9 96: s10, 100:s11, 104:t3, 108: t4, 112: t5, 116: t6 -120: mcause, 124: mepc, 128: mstatus -132-143: padding (12 bytes for 16-byte alignment) +120: mcause, 124: mepc, 128: mstatus, 132: sp (for restore) +136-143: padding (8 bytes for 16-byte alignment) ``` Why full context save in ISR? @@ -129,7 +129,7 @@ Why full context save in ISR? Each task stack must reserve space for the ISR frame: ```c -#define ISR_STACK_FRAME_SIZE 144 /* 33 words × 4 bytes, 16-byte aligned */ +#define ISR_STACK_FRAME_SIZE 144 /* 36 words × 4 bytes, 16-byte aligned */ ``` This "red zone" is reserved at the top of every task stack to guarantee ISR safety. diff --git a/Documentation/hal-riscv-context-switch.md b/Documentation/hal-riscv-context-switch.md index f66a41f..1a7f119 100644 --- a/Documentation/hal-riscv-context-switch.md +++ b/Documentation/hal-riscv-context-switch.md @@ -130,7 +130,7 @@ void *hal_build_initial_frame(void *stack_top, ISR_STACK_FRAME_SIZE); /* Initialize all general purpose registers to zero */ - for (int i = 0; i < 32; i++) + for (int i = 0; i < 36; i++) frame[i] = 0; /* Compute thread pointer: aligned to 64 bytes from _end */ diff --git a/app/umode.c b/app/umode.c index 518e111..0fe9eee 100644 --- a/app/umode.c +++ b/app/umode.c @@ -1,43 +1,73 @@ #include -/* U-mode Validation Task +/* U-mode validation: syscall stability and privilege isolation. * - * Integrates two tests into a single task flow to ensure sequential execution: - * 1. Phase 1: Mechanism Check - Verify syscalls work. - * 2. Phase 2: Security Check - Verify privileged instructions trigger a trap. + * Phase 1: Verify syscalls work under various SP conditions (normal, + * malicious). Phase 2: Verify privileged instructions trap. */ void umode_validation_task(void) { - /* --- Phase 1: Mechanism Check (Syscalls) --- */ - umode_printf("[umode] Phase 1: Testing Syscall Mechanism\n"); + /* --- Phase 1: Kernel Stack Isolation Test --- */ + umode_printf("[umode] Phase 1: Testing Kernel Stack Isolation\n"); + umode_printf("\n"); - /* Test 1: sys_tid() - Simplest read-only syscall. */ + /* Test 1a: Baseline - Syscall with normal SP */ + umode_printf("[umode] Test 1a: sys_tid() with normal SP\n"); int my_tid = sys_tid(); if (my_tid > 0) { umode_printf("[umode] PASS: sys_tid() returned %d\n", my_tid); } else { umode_printf("[umode] FAIL: sys_tid() failed (ret=%d)\n", my_tid); } + umode_printf("\n"); - /* Test 2: sys_uptime() - Verify value transmission is correct. */ + /* Test 1b: Verify ISR uses mscratch, not malicious user SP */ + umode_printf("[umode] Test 1b: sys_tid() with malicious SP\n"); + + uint32_t saved_sp; + asm volatile( + "mv %0, sp \n" + "li sp, 0xDEADBEEF \n" + : "=r"(saved_sp)); + + int my_tid_bad_sp = sys_tid(); + + asm volatile("mv sp, %0 \n" : : "r"(saved_sp)); + + if (my_tid_bad_sp > 0) { + umode_printf( + "[umode] PASS: sys_tid() succeeded, ISR correctly used kernel " + "stack\n"); + } else { + umode_printf( + "[umode] FAIL: Syscall failed with malicious SP (ret=%d)\n", + my_tid_bad_sp); + } + umode_printf("\n"); + + /* Test 1c: Verify syscall functionality is still intact */ + umode_printf("[umode] Test 1c: sys_uptime() with normal SP\n"); int uptime = sys_uptime(); if (uptime >= 0) { umode_printf("[umode] PASS: sys_uptime() returned %d\n", uptime); } else { umode_printf("[umode] FAIL: sys_uptime() failed (ret=%d)\n", uptime); } + umode_printf("\n"); - /* Note: Skipping sys_tadd for now, as kernel user pointer checks might - * block function pointers in the .text segment, avoiding distraction. - */ + umode_printf( + "[umode] Phase 1 Complete: Kernel stack isolation validated\n"); + umode_printf("\n"); /* --- Phase 2: Security Check (Privileged Access) --- */ umode_printf("[umode] ========================================\n"); + umode_printf("\n"); umode_printf("[umode] Phase 2: Testing Security Isolation\n"); + umode_printf("\n"); umode_printf( "[umode] Action: Attempting to read 'mstatus' CSR from U-mode.\n"); umode_printf("[umode] Expect: Kernel Panic with 'Illegal instruction'.\n"); - umode_printf("[umode] ========================================\n"); + umode_printf("\n"); /* CRITICAL: Delay before suicide to ensure logs are flushed from * buffer to UART. diff --git a/arch/riscv/boot.c b/arch/riscv/boot.c index 8e46f4c..afebb25 100644 --- a/arch/riscv/boot.c +++ b/arch/riscv/boot.c @@ -93,35 +93,32 @@ __attribute__((naked, section(".text.prologue"))) void _entry(void) : "memory"); } -/* Size of the full trap context frame saved on the stack by the ISR. - * 30 GPRs (x1, x3-x31) + mcause + mepc + mstatus = 33 words * 4 bytes = 132 - * bytes. Round up to 144 bytes for 16-byte alignment. +/* ISR trap frame layout (144 bytes = 36 words). + * [0-29]: GPRs (ra, gp, tp, t0-t6, s0-s11, a0-a7) + * [30]: mcause + * [31]: mepc + * [32]: mstatus + * [33]: SP (user SP in U-mode, original SP in M-mode) */ #define ISR_CONTEXT_SIZE 144 -/* Low-level Interrupt Service Routine (ISR) trampoline. - * - * This is the common entry point for all traps. It performs a FULL context - * save, creating a complete trap frame on the stack. This makes the C handler - * robust, as it does not need to preserve any registers itself. - */ +/* Low-level ISR common entry for all traps with full context save */ __attribute__((naked, aligned(4))) void _isr(void) { asm volatile( - /* Allocate stack frame for full context save */ + /* Blind swap with mscratch for kernel stack isolation. + * Convention: M-mode (mscratch=0, SP=kernel), U-mode (mscratch=kernel, + * SP=user). After swap: if SP != 0 came from U-mode, else M-mode. + */ + "csrrw sp, mscratch, sp\n" + "bnez sp, .Lumode_entry\n" + + /* Undo swap and continue for M-mode */ + "csrrw sp, mscratch, sp\n" + "addi sp, sp, -%0\n" - /* Save all general-purpose registers except x0 (zero) and x2 (sp). - * This includes caller-saved and callee-saved registers. - * - * Stack Frame Layout (offsets from sp in bytes): - * 0: ra, 4: gp, 8: tp, 12: t0, 16: t1, 20: t2 - * 24: s0, 28: s1, 32: a0, 36: a1, 40: a2, 44: a3 - * 48: a4, 52: a5, 56: a6, 60: a7, 64: s2, 68: s3 - * 72: s4, 76: s5, 80: s6, 84: s7, 88: s8, 92: s9 - * 96: s10, 100:s11, 104:t3, 108: t4, 112: t5, 116: t6 - * 120: mcause, 124: mepc - */ + /* Save all GPRs and CSRs */ "sw ra, 0*4(sp)\n" "sw gp, 1*4(sp)\n" "sw tp, 2*4(sp)\n" @@ -153,33 +150,149 @@ __attribute__((naked, aligned(4))) void _isr(void) "sw t5, 28*4(sp)\n" "sw t6, 29*4(sp)\n" - /* Save trap-related CSRs and prepare arguments for do_trap */ + /* Save original SP before frame allocation */ + "addi t0, sp, %0\n" + "sw t0, 33*4(sp)\n" + + /* Save machine CSRs (mcause, mepc, mstatus) */ "csrr a0, mcause\n" "csrr a1, mepc\n" - "csrr a2, mstatus\n" /* For context switching in privilege change */ - + "csrr a2, mstatus\n" "sw a0, 30*4(sp)\n" "sw a1, 31*4(sp)\n" "sw a2, 32*4(sp)\n" - "mv a2, sp\n" /* a2 = isr_sp */ - - /* Call the high-level C trap handler. - * Returns: a0 = SP to use for restoring context (may be different - * task's stack if context switch occurred). - */ + /* Call trap handler with frame pointer */ + "mv a2, sp\n" "call do_trap\n" + "mv sp, a0\n" + + /* Load mstatus and extract MPP to determine M-mode or U-mode return + path */ + "lw t0, 32*4(sp)\n" + "csrw mstatus, t0\n" + + "srli t1, t0, 11\n" + "andi t1, t1, 0x3\n" + "beqz t1, .Lrestore_umode\n" + + /* M-mode restore */ + ".Lrestore_mmode:\n" + "csrw mscratch, zero\n" + + "lw t1, 31*4(sp)\n" /* Restore mepc */ + "csrw mepc, t1\n" + + /* Restore all GPRs */ + "lw ra, 0*4(sp)\n" + "lw gp, 1*4(sp)\n" + "lw tp, 2*4(sp)\n" + "lw t0, 3*4(sp)\n" + "lw t1, 4*4(sp)\n" + "lw t2, 5*4(sp)\n" + "lw s0, 6*4(sp)\n" + "lw s1, 7*4(sp)\n" + "lw a0, 8*4(sp)\n" + "lw a1, 9*4(sp)\n" + "lw a2, 10*4(sp)\n" + "lw a3, 11*4(sp)\n" + "lw a4, 12*4(sp)\n" + "lw a5, 13*4(sp)\n" + "lw a6, 14*4(sp)\n" + "lw a7, 15*4(sp)\n" + "lw s2, 16*4(sp)\n" + "lw s3, 17*4(sp)\n" + "lw s4, 18*4(sp)\n" + "lw s5, 19*4(sp)\n" + "lw s6, 20*4(sp)\n" + "lw s7, 21*4(sp)\n" + "lw s8, 22*4(sp)\n" + "lw s9, 23*4(sp)\n" + "lw s10, 24*4(sp)\n" + "lw s11, 25*4(sp)\n" + "lw t3, 26*4(sp)\n" + "lw t4, 27*4(sp)\n" + "lw t5, 28*4(sp)\n" + "lw t6, 29*4(sp)\n" - /* Use returned SP for context restore (enables context switching) */ + /* Restore SP from frame[33] */ + "lw sp, 33*4(sp)\n" + + /* Return from trap */ + "mret\n" + + /* U-mode entry receives kernel stack in SP and user SP in mscratch */ + ".Lumode_entry:\n" + "addi sp, sp, -%0\n" + + /* Save t6 first to preserve it before using it as scratch */ + "sw t6, 29*4(sp)\n" + + /* Retrieve user SP from mscratch into t6 and save it */ + "csrr t6, mscratch\n" + "sw t6, 33*4(sp)\n" + + /* Save remaining GPRs */ + "sw ra, 0*4(sp)\n" + "sw gp, 1*4(sp)\n" + "sw tp, 2*4(sp)\n" + "sw t0, 3*4(sp)\n" + "sw t1, 4*4(sp)\n" + "sw t2, 5*4(sp)\n" + "sw s0, 6*4(sp)\n" + "sw s1, 7*4(sp)\n" + "sw a0, 8*4(sp)\n" + "sw a1, 9*4(sp)\n" + "sw a2, 10*4(sp)\n" + "sw a3, 11*4(sp)\n" + "sw a4, 12*4(sp)\n" + "sw a5, 13*4(sp)\n" + "sw a6, 14*4(sp)\n" + "sw a7, 15*4(sp)\n" + "sw s2, 16*4(sp)\n" + "sw s3, 17*4(sp)\n" + "sw s4, 18*4(sp)\n" + "sw s5, 19*4(sp)\n" + "sw s6, 20*4(sp)\n" + "sw s7, 21*4(sp)\n" + "sw s8, 22*4(sp)\n" + "sw s9, 23*4(sp)\n" + "sw s10, 24*4(sp)\n" + "sw s11, 25*4(sp)\n" + "sw t3, 26*4(sp)\n" + "sw t4, 27*4(sp)\n" + "sw t5, 28*4(sp)\n" + /* t6 already saved */ + + /* Save CSRs */ + "csrr a0, mcause\n" + "csrr a1, mepc\n" + "csrr a2, mstatus\n" + "sw a0, 30*4(sp)\n" + "sw a1, 31*4(sp)\n" + "sw a2, 32*4(sp)\n" + + "mv a2, sp\n" /* a2 = ISR frame pointer */ + "call do_trap\n" "mv sp, a0\n" - /* Restore mstatus from frame[32] */ + /* Check MPP in mstatus to determine return path */ "lw t0, 32*4(sp)\n" "csrw mstatus, t0\n" - /* Restore mepc from frame[31] (might have been modified by handler) */ + "srli t1, t0, 11\n" + "andi t1, t1, 0x3\n" + "bnez t1, .Lrestore_mmode\n" + + /* Setup mscratch for U-mode restore to prepare for next trap */ + ".Lrestore_umode:\n" + "la t1, _stack\n" + "csrw mscratch, t1\n" + "lw t1, 31*4(sp)\n" "csrw mepc, t1\n" + + /* Restore all GPRs */ "lw ra, 0*4(sp)\n" "lw gp, 1*4(sp)\n" "lw tp, 2*4(sp)\n" @@ -211,12 +324,12 @@ __attribute__((naked, aligned(4))) void _isr(void) "lw t5, 28*4(sp)\n" "lw t6, 29*4(sp)\n" - /* Deallocate stack frame */ - "addi sp, sp, %0\n" + /* Restore user SP from frame[33] */ + "lw sp, 33*4(sp)\n" /* Return from trap */ "mret\n" - : /* no outputs */ - : "i"(ISR_CONTEXT_SIZE) /* +16 for mcause, mepc, mstatus */ + : /* no outputs */ + : "i"(ISR_CONTEXT_SIZE) : "memory"); } diff --git a/arch/riscv/hal.c b/arch/riscv/hal.c index 7ad5806..93abe3d 100644 --- a/arch/riscv/hal.c +++ b/arch/riscv/hal.c @@ -6,6 +6,12 @@ #include "private/stdio.h" #include "private/utils.h" +/* Kernel stack pointer for U-mode ISR isolation. + * When U-mode task runs, mscratch points to this value. + * When M-mode runs, mscratch is zero. + */ +uint32_t _kernel_stack_ptr = 0; + /* Context frame offsets for jmp_buf (as 32-bit word indices). * * This layout defines the structure of the jmp_buf. The first 16 elements @@ -48,39 +54,40 @@ * Indices are in word offsets (divide byte offset by 4). */ enum { - FRAME_RA = 0, /* x1 - Return Address */ - FRAME_GP = 1, /* x3 - Global Pointer */ - FRAME_TP = 2, /* x4 - Thread Pointer */ - FRAME_T0 = 3, /* x5 - Temporary register 0 */ - FRAME_T1 = 4, /* x6 - Temporary register 1 */ - FRAME_T2 = 5, /* x7 - Temporary register 2 */ - FRAME_S0 = 6, /* x8 - Saved register 0 / Frame Pointer */ - FRAME_S1 = 7, /* x9 - Saved register 1 */ - FRAME_A0 = 8, /* x10 - Argument/Return 0 */ - FRAME_A1 = 9, /* x11 - Argument/Return 1 */ - FRAME_A2 = 10, /* x12 - Argument 2 */ - FRAME_A3 = 11, /* x13 - Argument 3 */ - FRAME_A4 = 12, /* x14 - Argument 4 */ - FRAME_A5 = 13, /* x15 - Argument 5 */ - FRAME_A6 = 14, /* x16 - Argument 6 */ - FRAME_A7 = 15, /* x17 - Argument 7 / Syscall Number */ - FRAME_S2 = 16, /* x18 - Saved register 2 */ - FRAME_S3 = 17, /* x19 - Saved register 3 */ - FRAME_S4 = 18, /* x20 - Saved register 4 */ - FRAME_S5 = 19, /* x21 - Saved register 5 */ - FRAME_S6 = 20, /* x22 - Saved register 6 */ - FRAME_S7 = 21, /* x23 - Saved register 7 */ - FRAME_S8 = 22, /* x24 - Saved register 8 */ - FRAME_S9 = 23, /* x25 - Saved register 9 */ - FRAME_S10 = 24, /* x26 - Saved register 10 */ - FRAME_S11 = 25, /* x27 - Saved register 11 */ - FRAME_T3 = 26, /* x28 - Temporary register 3 */ - FRAME_T4 = 27, /* x29 - Temporary register 4 */ - FRAME_T5 = 28, /* x30 - Temporary register 5 */ - FRAME_T6 = 29, /* x31 - Temporary register 6 */ - FRAME_MCAUSE = 30, /* Machine Cause CSR */ - FRAME_EPC = 31, /* Machine Exception PC (mepc) */ - FRAME_MSTATUS = 32 /* Machine Status CSR */ + FRAME_RA = 0, /* x1 - Return Address */ + FRAME_GP = 1, /* x3 - Global Pointer */ + FRAME_TP = 2, /* x4 - Thread Pointer */ + FRAME_T0 = 3, /* x5 - Temporary register 0 */ + FRAME_T1 = 4, /* x6 - Temporary register 1 */ + FRAME_T2 = 5, /* x7 - Temporary register 2 */ + FRAME_S0 = 6, /* x8 - Saved register 0 / Frame Pointer */ + FRAME_S1 = 7, /* x9 - Saved register 1 */ + FRAME_A0 = 8, /* x10 - Argument/Return 0 */ + FRAME_A1 = 9, /* x11 - Argument/Return 1 */ + FRAME_A2 = 10, /* x12 - Argument 2 */ + FRAME_A3 = 11, /* x13 - Argument 3 */ + FRAME_A4 = 12, /* x14 - Argument 4 */ + FRAME_A5 = 13, /* x15 - Argument 5 */ + FRAME_A6 = 14, /* x16 - Argument 6 */ + FRAME_A7 = 15, /* x17 - Argument 7 / Syscall Number */ + FRAME_S2 = 16, /* x18 - Saved register 2 */ + FRAME_S3 = 17, /* x19 - Saved register 3 */ + FRAME_S4 = 18, /* x20 - Saved register 4 */ + FRAME_S5 = 19, /* x21 - Saved register 5 */ + FRAME_S6 = 20, /* x22 - Saved register 6 */ + FRAME_S7 = 21, /* x23 - Saved register 7 */ + FRAME_S8 = 22, /* x24 - Saved register 8 */ + FRAME_S9 = 23, /* x25 - Saved register 9 */ + FRAME_S10 = 24, /* x26 - Saved register 10 */ + FRAME_S11 = 25, /* x27 - Saved register 11 */ + FRAME_T3 = 26, /* x28 - Temporary register 3 */ + FRAME_T4 = 27, /* x29 - Temporary register 4 */ + FRAME_T5 = 28, /* x30 - Temporary register 5 */ + FRAME_T6 = 29, /* x31 - Temporary register 6 */ + FRAME_MCAUSE = 30, /* Machine Cause CSR */ + FRAME_EPC = 31, /* Machine Exception PC (mepc) */ + FRAME_MSTATUS = 32, /* Machine Status CSR */ + FRAME_SP = 33 /* Stack Pointer (saved for restore) */ }; /* Global variable to hold the new stack pointer for pending context switch. @@ -294,6 +301,12 @@ void hal_hardware_init(void) "csrw pmpcfg0, %1\n" : : "r"(pmpaddr), "r"(pmpcfg)); + + /* Initialize kernel stack pointer for U-mode isolation. + * This value is used by the ISR epilogue to restore mscratch + * when returning to U-mode tasks. _stack is the kernel stack base. + */ + _kernel_stack_ptr = (uint32_t) &_stack; } /* Halts the system in an unrecoverable state */ @@ -508,20 +521,16 @@ void *hal_build_initial_frame(void *stack_top, ISR_STACK_FRAME_SIZE); /* Zero out entire frame */ - for (int i = 0; i < 32; i++) { + for (int i = 0; i < 36; i++) { frame[i] = 0; } /* Compute tp value same as boot.c: aligned to 64 bytes from _end */ uint32_t tp_val = ((uint32_t) &_end + 63) & ~63U; - /* Initialize critical registers for proper task startup: - * - frame[1] = gp: Global pointer, required for accessing global variables - * - frame[2] = tp: Thread pointer, required for thread-local storage - * - frame[32] = mepc: Task entry point, where mret will jump to - */ - frame[1] = (uint32_t) &_gp; /* gp - global pointer */ - frame[2] = tp_val; /* tp - thread pointer */ + /* Initialize critical registers for proper task startup */ + frame[FRAME_GP] = (uint32_t) &_gp; /* gp - global pointer */ + frame[FRAME_TP] = tp_val; /* tp - thread pointer */ /* Initialize mstatus for new task: * - MPIE=1: mret will copy this to MIE, enabling interrupts after task @@ -534,6 +543,10 @@ void *hal_build_initial_frame(void *stack_top, frame[FRAME_MSTATUS] = mstatus_val; frame[FRAME_EPC] = (uint32_t) task_entry; /* mepc - entry point */ + /* SP value for when ISR returns. Must match the deallocation in + * __dispatch_init_isr which adds ISR_STACK_FRAME_SIZE to frame pointer. + */ + frame[FRAME_SP] = (uint32_t) ((uint8_t *) frame + ISR_STACK_FRAME_SIZE); return (void *) frame; } @@ -811,7 +824,25 @@ static void __attribute__((naked, used)) __dispatch_init_isr(void) "lw t0, 32*4(sp)\n" "csrw mstatus, t0\n" - /* Restore mepc from frame[31] */ + /* Initialize mscratch based on MPP field in mstatus. + * For M-mode set mscratch to zero, for U-mode set to kernel stack. + * ISR uses this to detect privilege mode via blind swap. + */ + "srli t2, t0, 11\n" + "andi t2, t2, 0x3\n" + "bnez t2, .Ldispatch_mmode\n" + + /* U-mode path */ + "la t2, _stack\n" + "csrw mscratch, t2\n" + "j .Ldispatch_done\n" + + /* M-mode path */ + ".Ldispatch_mmode:\n" + "csrw mscratch, zero\n" + ".Ldispatch_done:\n" + + /* Restore mepc */ "lw t1, 31*4(sp)\n" "csrw mepc, t1\n" diff --git a/arch/riscv/hal.h b/arch/riscv/hal.h index 7946a0f..1c406ba 100644 --- a/arch/riscv/hal.h +++ b/arch/riscv/hal.h @@ -3,6 +3,8 @@ #include /* Symbols from the linker script, defining memory boundaries */ +extern uint32_t _gp; /* Global pointer initialized at reset */ +extern uint32_t _stack; /* Kernel stack top for ISR and boot */ extern uint32_t _stack_start, _stack_end; /* Start/end of the STACK memory */ extern uint32_t _heap_start, _heap_end; /* Start/end of the HEAP memory */ extern uint32_t _heap_size; /* Size of HEAP memory */ diff --git a/kernel/syscall.c b/kernel/syscall.c index 7be6663..c9cd243 100644 --- a/kernel/syscall.c +++ b/kernel/syscall.c @@ -384,16 +384,21 @@ int sys_uptime(void) } /* User mode safe output syscall. - * Outputs a string from user mode by executing puts() in kernel context. - * This avoids privilege violations from printf's logger mutex operations. + * Outputs a string from user mode directly via UART, bypassing the logger + * queue. Direct output ensures strict ordering for U-mode tasks and avoids race + * conditions with the async logger task. */ static int _tputs(const char *str) { if (unlikely(!str)) return -EINVAL; - /* Use puts() which will handle logger enqueue or direct output */ - return puts(str); + /* Output directly character by character. U-mode tasks need strict + * ordering, which the async logger queue cannot guarantee. */ + for (const char *p = str; *p; p++) + _putchar(*p); + + return 0; } int sys_tputs(const char *str)