Try   HackMD

Multithreading for RISC-V

李漢德

Mission

My term project involves building a mini operating system and extending it to include POSIX thread functionality. I will divide it into two parts: the first part focuses on the operating system itself, while the second part incorporates threading.

Part I mini os

kernel.c

#include "kernel.h"
#include "common.h"

typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef uint32_t size_t;

extern char __bss[], __bss_end[], __stack_top[];

void yield(void);
void proc_b_entry(void);
void proc_a_entry(void);
void user_entry(void);
void map_page(uint32_t *table1, uint32_t vaddr, paddr_t paddr, uint32_t flags);
void handle_syscall(struct trap_frame *f);
long getchar(void);
void fs_flush(void);
void read_write_disk(void *buf, unsigned sector, int is_write);
struct file *fs_lookup(const char *filename);
struct virtio_virtq *virtq_init(unsigned index);

// Boot
__attribute__((section(".text.boot")))
__attribute__((naked))
void boot(void) {
    __asm__ __volatile__(
        "mv sp, %[stack_top]\n"
        "j kernel_main\n"
        :
        : [stack_top] "r" (__stack_top)
    );
}

// SBI
struct sbiret sbi_call(long arg0, long arg1, long arg2, long arg3,
                       long arg4, long arg5, long fid, long eid) {
    register long a0 __asm__("a0") = arg0;
    register long a1 __asm__("a1") = arg1;
    register long a2 __asm__("a2") = arg2;
    register long a3 __asm__("a3") = arg3;
    register long a4 __asm__("a4") = arg4;
    register long a5 __asm__("a5") = arg5;
    register long a6 __asm__("a6") = fid;
    register long a7 __asm__("a7") = eid;

    __asm__ __volatile__("ecall"
                         : "=r"(a0), "=r"(a1)
                         : "r"(a0), "r"(a1), "r"(a2), "r"(a3),
                           "r"(a4), "r"(a5), "r"(a6), "r"(a7)
                         : "memory");
    return (struct sbiret){.error = a0, .value = a1};
}

void putchar(char ch) {
    sbi_call(ch, 0, 0, 0, 0, 0, 0, 1 /* SBI console putchar */);
}

// kernel_entry
__attribute__((naked))
__attribute__((aligned(4)))
void kernel_entry(void) {
    __asm__ __volatile__(
        "csrw sscratch, sp\n"
        "addi sp, sp, -4 * 31\n"
        "sw ra,  4 * 0(sp)\n"
        "sw gp,  4 * 1(sp)\n"
        "sw tp,  4 * 2(sp)\n"
        "sw t0,  4 * 3(sp)\n"
        "sw t1,  4 * 4(sp)\n"
        "sw t2,  4 * 5(sp)\n"
        "sw t3,  4 * 6(sp)\n"
        "sw t4,  4 * 7(sp)\n"
        "sw t5,  4 * 8(sp)\n"
        "sw t6,  4 * 9(sp)\n"
        "sw a0,  4 * 10(sp)\n"
        "sw a1,  4 * 11(sp)\n"
        "sw a2,  4 * 12(sp)\n"
        "sw a3,  4 * 13(sp)\n"
        "sw a4,  4 * 14(sp)\n"
        "sw a5,  4 * 15(sp)\n"
        "sw a6,  4 * 16(sp)\n"
        "sw a7,  4 * 17(sp)\n"
        "sw s0,  4 * 18(sp)\n"
        "sw s1,  4 * 19(sp)\n"
        "sw s2,  4 * 20(sp)\n"
        "sw s3,  4 * 21(sp)\n"
        "sw s4,  4 * 22(sp)\n"
        "sw s5,  4 * 23(sp)\n"
        "sw s6,  4 * 24(sp)\n"
        "sw s7,  4 * 25(sp)\n"
        "sw s8,  4 * 26(sp)\n"
        "sw s9,  4 * 27(sp)\n"
        "sw s10, 4 * 28(sp)\n"
        "sw s11, 4 * 29(sp)\n"

        "csrr a0, sscratch\n"
        "sw a0, 4 * 30(sp)\n"

        // Reset the kernel stack
        "addi a0, sp, 4 * 31\n"
        "csrw sscratch, a0\n"

        "mv a0, sp\n"
        "call handle_trap\n"

        "lw ra,  4 * 0(sp)\n"
        "lw gp,  4 * 1(sp)\n"
        "lw tp,  4 * 2(sp)\n"
        "lw t0,  4 * 3(sp)\n"
        "lw t1,  4 * 4(sp)\n"
        "lw t2,  4 * 5(sp)\n"
        "lw t3,  4 * 6(sp)\n"
        "lw t4,  4 * 7(sp)\n"
        "lw t5,  4 * 8(sp)\n"
        "lw t6,  4 * 9(sp)\n"
        "lw a0,  4 * 10(sp)\n"
        "lw a1,  4 * 11(sp)\n"
        "lw a2,  4 * 12(sp)\n"
        "lw a3,  4 * 13(sp)\n"
        "lw a4,  4 * 14(sp)\n"
        "lw a5,  4 * 15(sp)\n"
        "lw a6,  4 * 16(sp)\n"
        "lw a7,  4 * 17(sp)\n"
        "lw s0,  4 * 18(sp)\n"
        "lw s1,  4 * 19(sp)\n"
        "lw s2,  4 * 20(sp)\n"
        "lw s3,  4 * 21(sp)\n"
        "lw s4,  4 * 22(sp)\n"
        "lw s5,  4 * 23(sp)\n"
        "lw s6,  4 * 24(sp)\n"
        "lw s7,  4 * 25(sp)\n"
        "lw s8,  4 * 26(sp)\n"
        "lw s9,  4 * 27(sp)\n"
        "lw s10, 4 * 28(sp)\n"
        "lw s11, 4 * 29(sp)\n"
        "lw sp,  4 * 30(sp)\n"
        "sret\n"
    );
}

// trap
void handle_trap(struct trap_frame *f) {
    uint32_t scause = READ_CSR(scause);
    uint32_t stval  = READ_CSR(stval);
    uint32_t user_pc= READ_CSR(sepc);

    if (scause == SCAUSE_ECALL) {
        handle_syscall(f);
        user_pc += 4;
    } else {
        PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n",
              scause, stval, user_pc);
    }
    WRITE_CSR(sepc, user_pc);
}

// alloc_pages
extern char __free_ram[], __free_ram_end[];

paddr_t alloc_pages(uint32_t n) {
    static paddr_t next_paddr = (paddr_t)__free_ram;
    paddr_t paddr = next_paddr;
    next_paddr += n * PAGE_SIZE;

    if (next_paddr > (paddr_t)__free_ram_end) {
        PANIC("out of memory");
    }
    memset((void *)paddr, 0, n * PAGE_SIZE);
    return paddr;
}

// switch_context
__attribute__((naked))
void switch_context(uint32_t *prev_sp, uint32_t *next_sp) {
    __asm__ __volatile__(
        "addi sp, sp, -13 * 4\n"
        "sw ra,  0  * 4(sp)\n"
        "sw s0,  1  * 4(sp)\n"
        "sw s1,  2  * 4(sp)\n"
        "sw s2,  3  * 4(sp)\n"
        "sw s3,  4  * 4(sp)\n"
        "sw s4,  5  * 4(sp)\n"
        "sw s5,  6  * 4(sp)\n"
        "sw s6,  7  * 4(sp)\n"
        "sw s7,  8  * 4(sp)\n"
        "sw s8,  9  * 4(sp)\n"
        "sw s9,  10 * 4(sp)\n"
        "sw s10, 11 * 4(sp)\n"
        "sw s11, 12 * 4(sp)\n"

        "sw sp, (a0)\n"
        "lw sp, (a1)\n"

        "lw ra,  0  * 4(sp)\n"
        "lw s0,  1  * 4(sp)\n"
        "lw s1,  2  * 4(sp)\n"
        "lw s2,  3  * 4(sp)\n"
        "lw s3,  4  * 4(sp)\n"
        "lw s4,  5  * 4(sp)\n"
        "lw s5,  6  * 4(sp)\n"
        "lw s6,  7  * 4(sp)\n"
        "lw s7,  8  * 4(sp)\n"
        "lw s8,  9  * 4(sp)\n"
        "lw s9,  10 * 4(sp)\n"
        "lw s10, 11 * 4(sp)\n"
        "lw s11, 12 * 4(sp)\n"
        "addi sp, sp, 13 * 4\n"
        "ret\n"
    );
}

#define PROCS_MAX 8
#define PROC_UNUSED  0
#define PROC_RUNNABLE 1
#define PROC_EXITED 2

struct process procs[PROCS_MAX];
extern char __kernel_base[];

struct process *current_proc;
struct process *idle_proc;

void map_page(uint32_t *table1, uint32_t vaddr, paddr_t paddr, uint32_t flags);

// create_process
struct process *create_process(const void *image, size_t image_size) {
    struct process *proc = NULL;
    for (int i = 0; i < PROCS_MAX; i++) {
        if (procs[i].state == PROC_UNUSED) {
            proc = &procs[i];
            break;
        }
    }
    if (!proc) {
        PANIC("no free process slots");
    }
    uint32_t *sp = (uint32_t *)&proc->stack[sizeof(proc->stack)];
    // stack frames for callee-saved regs
    for (int r = 0; r < 12; r++) {
        *--sp = 0;
    }
    *--sp = (uint32_t)user_entry; // ra

    // build page table
    uint32_t *page_table = (uint32_t *)alloc_pages(1);
    // map kernel
    for (paddr_t paddr = (paddr_t)__kernel_base;
         paddr < (paddr_t)__free_ram_end; paddr += PAGE_SIZE) {
        map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X);
    }
    // map virtio-blk
    map_page(page_table, VIRTIO_BLK_PADDR, VIRTIO_BLK_PADDR, PAGE_R | PAGE_W);

    // map user image
    for (uint32_t off = 0; off < image_size; off += PAGE_SIZE) {
        paddr_t page = alloc_pages(1);
        size_t remain = image_size - off;
        size_t cpysz = (remain > PAGE_SIZE) ? PAGE_SIZE : remain;
        memcpy((void *)page, image + off, cpysz);
        map_page(page_table, USER_BASE + off, page,
                 PAGE_U | PAGE_R | PAGE_W | PAGE_X);
    }

    proc->pid = 1; // or i+1
    proc->state = PROC_RUNNABLE;
    proc->sp = (uint32_t)sp;
    proc->page_table = page_table;
    return proc;
}

// yield
void yield(void) {
    struct process *next = idle_proc;
    for (int i = 0; i < PROCS_MAX; i++) {
        struct process *p =
            &procs[(current_proc->pid + i) % PROCS_MAX];
        if (p->state == PROC_RUNNABLE && p->pid > 0) {
            next = p;
            break;
        }
    }
    if (next == current_proc) {
        return;
    }
    __asm__ __volatile__(
        "sfence.vma\n"
        "csrw satp, %[satp]\n"
        "sfence.vma\n"
        "csrw sscratch, %[sscratch]\n"
        :
        : [satp] "r"(SATP_SV32 | ((uint32_t)next->page_table / PAGE_SIZE)),
          [sscratch] "r"((uint32_t)&next->stack[sizeof(next->stack)])
    );

    struct process *prev = current_proc;
    current_proc = next;
    switch_context(&prev->sp, &next->sp);
}

// map_page
void map_page(uint32_t *table1, uint32_t vaddr, paddr_t paddr, uint32_t flags) {
    if (!is_aligned(vaddr, PAGE_SIZE)) {
        PANIC("unaligned vaddr %x", vaddr);
    }
    if (!is_aligned(paddr, PAGE_SIZE)) {
        PANIC("unaligned paddr %x", paddr);
    }
    uint32_t vpn1 = (vaddr >> 22) & 0x3ff;
    if ((table1[vpn1] & PAGE_V) == 0) {
        uint32_t pt_paddr = alloc_pages(1);
        table1[vpn1] = ((pt_paddr / PAGE_SIZE) << 10) | PAGE_V;
    }
    uint32_t vpn0 = (vaddr >> 12) & 0x3ff;
    uint32_t *table0 = (uint32_t *)((table1[vpn1] >> 10) * PAGE_SIZE);
    table0[vpn0] = ((paddr / PAGE_SIZE) << 10) | flags | PAGE_V;
}

// user_entry
__attribute__((naked))
void user_entry(void) {
    __asm__ __volatile__(
        "csrw sepc, %[sepc]\n"
        "csrw sstatus, %[sstatus]\n"
        "sret\n"
        :
        : [sepc] "r"(USER_BASE),
          [sstatus] "r"(SSTATUS_SPIE | SSTATUS_SUM)
    );
}

// handle_syscall
void handle_syscall(struct trap_frame *f) {
    switch (f->a3) {
    case SYS_GETCHAR:
        while (1) {
            long ch = getchar();
            if (ch >= 0) {
                f->a0 = ch;
                break;
            }
            yield();
        }
        break;
    case SYS_EXIT:
        printf("process %d exited\n", current_proc->pid);
        current_proc->state = PROC_EXITED;
        yield();
        PANIC("unreachable");
    case SYS_PUTCHAR:
        putchar(f->a0);
        break;
    case SYS_READFILE:
    case SYS_WRITEFILE: {
        const char *filename = (const char *)f->a0;
        char *buf = (char *)f->a1;
        int len = f->a2;
        struct file *file = fs_lookup(filename);
        if (!file) {
            printf("file not found: %s\n", filename);
            f->a0 = -1;
            break;
        }
        if (len > (int)file->size && f->a3 == SYS_READFILE) {
            len = file->size;
        } else if (len > (int)sizeof(file->data)) {
            len = (int)sizeof(file->data);
        }
        if (f->a3 == SYS_WRITEFILE) {
            memcpy(file->data, buf, len);
            file->size = len;
            fs_flush();
        } else {
            memcpy(buf, file->data, len);
        }
        f->a0 = len; // return the number of bytes read/written
    } break;
    default:
        PANIC("unexpected syscall a3=%x\n", f->a3);
    }
}

// getchar
long getchar(void) {
    struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2);
    return ret.error;
}

// virtio-blk
struct virtio_virtq *blk_request_vq;
struct virtio_blk_req *blk_req;
paddr_t blk_req_paddr;
unsigned blk_capacity;

uint32_t virtio_reg_read32(unsigned offset) {
    return *((volatile uint32_t *)(VIRTIO_BLK_PADDR + offset));
}
uint64_t virtio_reg_read64(unsigned offset) {
    return *((volatile uint64_t *)(VIRTIO_BLK_PADDR + offset));
}
void virtio_reg_write32(unsigned offset, uint32_t value) {
    *((volatile uint32_t *)(VIRTIO_BLK_PADDR + offset)) = value;
}
void virtio_reg_fetch_and_or32(unsigned offset, uint32_t value) {
    virtio_reg_write32(offset, virtio_reg_read32(offset) | value);
}

void virtio_blk_init(void) {
    if (virtio_reg_read32(VIRTIO_REG_MAGIC) != 0x74726976)
        PANIC("virtio: invalid magic value");
    if (virtio_reg_read32(VIRTIO_REG_VERSION) != 1)
        PANIC("virtio: invalid version");
    if (virtio_reg_read32(VIRTIO_REG_DEVICE_ID) != VIRTIO_DEVICE_BLK)
        PANIC("virtio: invalid device id");

    virtio_reg_write32(VIRTIO_REG_DEVICE_STATUS, 0);
    virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_ACK);
    virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_DRIVER);
    virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_FEAT_OK);

    blk_request_vq = virtq_init(0);
    virtio_reg_write32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_DRIVER_OK);

    blk_capacity = virtio_reg_read64(VIRTIO_REG_DEVICE_CONFIG + 0) * SECTOR_SIZE;
    printf("virtio-blk: capacity is %d bytes\n", blk_capacity);

    blk_req_paddr = alloc_pages(align_up(sizeof(*blk_req), PAGE_SIZE) / PAGE_SIZE);
    blk_req = (struct virtio_blk_req *)blk_req_paddr;
}

struct virtio_virtq *virtq_init(unsigned index) {
    paddr_t virtq_paddr = alloc_pages(align_up(sizeof(struct virtio_virtq),
                                               PAGE_SIZE) / PAGE_SIZE);
    struct virtio_virtq *vq = (struct virtio_virtq *)virtq_paddr;
    vq->queue_index = index;
    vq->used_index  = (volatile uint16_t *)&vq->used.index;

    virtio_reg_write32(VIRTIO_REG_QUEUE_SEL, index);
    virtio_reg_write32(VIRTIO_REG_QUEUE_NUM, VIRTQ_ENTRY_NUM);
    virtio_reg_write32(VIRTIO_REG_QUEUE_ALIGN, 0);
    virtio_reg_write32(VIRTIO_REG_QUEUE_PFN, virtq_paddr);

    return vq;
}

void virtq_kick(struct virtio_virtq *vq, int desc_index) {
    vq->avail.ring[vq->avail.index % VIRTQ_ENTRY_NUM] = desc_index;
    vq->avail.index++;
    __sync_synchronize();
    virtio_reg_write32(VIRTIO_REG_QUEUE_NOTIFY, vq->queue_index);
    vq->last_used_index++;
}

bool virtq_is_busy(struct virtio_virtq *vq) {
    return vq->last_used_index != *vq->used_index;
}

void read_write_disk(void *buf, unsigned sector, int is_write) {
    if (sector >= blk_capacity / SECTOR_SIZE) {
        printf("virtio: tried to read/write sector=%d, but capacity=%d\n",
               sector, blk_capacity / SECTOR_SIZE);
        return;
    }
    blk_req->sector = sector;
    blk_req->type = is_write ? VIRTIO_BLK_T_OUT : VIRTIO_BLK_T_IN;
    if (is_write) {
        memcpy(blk_req->data, buf, SECTOR_SIZE);
    }

    struct virtio_virtq *vq = blk_request_vq;
    vq->descs[0].addr  = blk_req_paddr;
    vq->descs[0].len   = sizeof(uint32_t)*2 + sizeof(uint64_t);
    vq->descs[0].flags = VIRTQ_DESC_F_NEXT;
    vq->descs[0].next  = 1;

    vq->descs[1].addr  = blk_req_paddr + offsetof(struct virtio_blk_req, data);
    vq->descs[1].len   = SECTOR_SIZE;
    vq->descs[1].flags = VIRTQ_DESC_F_NEXT | (is_write ? 0 : VIRTQ_DESC_F_WRITE);
    vq->descs[1].next  = 2;

    vq->descs[2].addr  = blk_req_paddr + offsetof(struct virtio_blk_req, status);
    vq->descs[2].len   = sizeof(uint8_t);
    vq->descs[2].flags = VIRTQ_DESC_F_WRITE;

    virtq_kick(vq, 0);

    while (virtq_is_busy(vq)) { ; }

    if (blk_req->status != 0) {
        printf("virtio: warn: read/write sector=%d status=%d\n",
               sector, blk_req->status);
        return;
    }
    if (!is_write) {
        memcpy(buf, blk_req->data, SECTOR_SIZE);
    }
}

// FS
struct file files[FILES_MAX];
uint8_t disk[DISK_MAX_SIZE];

int oct2int(char *oct, int len) {
    int dec = 0;
    for (int i = 0; i < len; i++) {
        if (oct[i] < '0' || oct[i] > '7')
            break;
        dec = dec * 8 + (oct[i] - '0');
    }
    return dec;
}

// fs_init
void fs_init(void) {
    // read all disk sectors to disk[]
    for (unsigned sector = 0; sector < sizeof(disk) / SECTOR_SIZE; sector++) {
        read_write_disk(&disk[sector * SECTOR_SIZE], sector, 0);
    }
    unsigned off = 0;
    for (int i = 0; i < FILES_MAX; i++) {
        struct tar_header *header = (struct tar_header *)&disk[off];
        if (header->name[0] == '\0') {
            break;
        }
        if (strcmp(header->magic, "ustar") != 0) {
            PANIC("invalid tar header: magic=\"%s\"", header->magic);
        }
        int filesz = oct2int(header->size, sizeof(header->size));

        // remove leading "./" if exists
        char *nameptr = header->name;
        if (nameptr[0] == '.' && nameptr[1] == '/') {
            nameptr += 2;
        }

        struct file *file = &files[i];
        file->in_use = true;
        strcpy(file->name, nameptr);
        memcpy(file->data, header->data, filesz);
        file->size = filesz;

        printf("file: %s, size=%d\n", file->name, file->size);

        off += align_up(sizeof(struct tar_header) + filesz, SECTOR_SIZE);
        if (off >= sizeof(disk)) {
            break;
        }
    }
}

void fs_flush(void) {
    memset(disk, 0, sizeof(disk));
    unsigned off = 0;
    for (int i = 0; i < FILES_MAX; i++) {
        struct file *file = &files[i];
        if (!file->in_use) continue;

        struct tar_header *header = (struct tar_header *)&disk[off];
        memset(header, 0, sizeof(*header));
        // store name
        strcpy(header->name, file->name);

        // minimal fields
        strcpy(header->mode, "000644");
        strcpy(header->magic, "ustar");
        strcpy(header->version, "00");
        header->type = '0';

        // size in octal
        int filesz = file->size;
        for (int j = sizeof(header->size); j > 0; j--) {
            header->size[j - 1] = (filesz % 8) + '0';
            filesz /= 8;
        }

        // checksum
        int checksum = ' ' * sizeof(header->checksum);
        for (unsigned k = 0; k < sizeof(struct tar_header); k++) {
            checksum += (unsigned char)disk[off + k];
        }
        for (int j = 5; j >= 0; j--) {
            header->checksum[j] = (checksum % 8) + '0';
            checksum /= 8;
        }

        // copy data
        memcpy(header->data, file->data, file->size);

        off += align_up(sizeof(struct tar_header) + file->size, SECTOR_SIZE);
        if (off >= sizeof(disk)) {
            break;
        }
    }
    // write disk[] back
    for (unsigned sector = 0; sector < sizeof(disk) / SECTOR_SIZE; sector++) {
        read_write_disk(&disk[sector * SECTOR_SIZE], sector, 1);
    }
    printf("wrote %d bytes to disk\n", (int)sizeof(disk));
}

struct file *fs_lookup(const char *filename) {
    for (int i = 0; i < FILES_MAX; i++) {
        struct file *f = &files[i];
        if (f->in_use && strcmp(f->name, filename) == 0) {
            return f;
        }
    }
    return NULL;
}

// kernel_main
extern char _binary_shell_bin_start[], _binary_shell_bin_size[];

void kernel_main(void) {
    memset(__bss, 0, (size_t)__bss_end - (size_t)__bss);

    printf("\n\n");
    WRITE_CSR(stvec, (uint32_t)kernel_entry);

    virtio_blk_init();

    // test read/write sector 0
    char buf[SECTOR_SIZE];
    read_write_disk(buf, 0, 0);
    printf("first sector: %s\n", buf);

    // strcpy(buf, "hello from kernel!!!\n");
    // read_write_disk(buf, 0, 1);

    fs_init();

    // create idle
    idle_proc = create_process(NULL, 0);
    idle_proc->pid = -1;
    current_proc = idle_proc;

    // create shell
    create_process(_binary_shell_bin_start, (size_t)_binary_shell_bin_size);

    yield();
    PANIC("switched to idle process");
}

kernel.h

#pragma once #include "common.h" struct sbiret { long error; long value; }; #define PANIC(fmt, ...) \ do { \ printf("PANIC: %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ while (1) {} \ } while (0) struct trap_frame { uint32_t ra; uint32_t gp; uint32_t tp; uint32_t t0; uint32_t t1; uint32_t t2; uint32_t t3; uint32_t t4; uint32_t t5; uint32_t t6; uint32_t a0; uint32_t a1; uint32_t a2; uint32_t a3; uint32_t a4; uint32_t a5; uint32_t a6; uint32_t a7; uint32_t s0; uint32_t s1; uint32_t s2; uint32_t s3; uint32_t s4; uint32_t s5; uint32_t s6; uint32_t s7; uint32_t s8; uint32_t s9; uint32_t s10; uint32_t s11; uint32_t sp; } __attribute__((packed)); // CSRs #define READ_CSR(reg) ({ \ unsigned long __tmp; \ __asm__ __volatile__("csrr %0, " #reg : "=r"(__tmp)); \ __tmp; \ }) #define WRITE_CSR(reg, value) do { \ uint32_t __tmp = (value); \ __asm__ __volatile__("csrw " #reg ", %0" :: "r"(__tmp)); \ } while (0) #define SATP_SV32 (1u << 31) #define PAGE_V (1 << 0) #define PAGE_R (1 << 1) #define PAGE_W (1 << 2) #define PAGE_X (1 << 3) #define PAGE_U (1 << 4) #define SSTATUS_SPIE (1 << 5) #define SCAUSE_ECALL 8 #define SSTATUS_SUM (1 << 18) // Process struct process { int pid; int state; vaddr_t sp; uint32_t *page_table; uint8_t stack[8192]; }; // For process states #define PROC_UNUSED 0 #define PROC_RUNNABLE 1 #define PROC_EXITED 2 #define USER_BASE 0x1000000 // VirtIO #define SECTOR_SIZE 512 #define VIRTQ_ENTRY_NUM 16 #define VIRTIO_DEVICE_BLK 2 #define VIRTIO_BLK_PADDR 0x10001000 #define VIRTIO_REG_MAGIC 0x00 #define VIRTIO_REG_VERSION 0x04 #define VIRTIO_REG_DEVICE_ID 0x08 #define VIRTIO_REG_QUEUE_SEL 0x30 #define VIRTIO_REG_QUEUE_NUM_MAX 0x34 #define VIRTIO_REG_QUEUE_NUM 0x38 #define VIRTIO_REG_QUEUE_ALIGN 0x3c #define VIRTIO_REG_QUEUE_PFN 0x40 #define VIRTIO_REG_QUEUE_READY 0x44 #define VIRTIO_REG_QUEUE_NOTIFY 0x50 #define VIRTIO_REG_DEVICE_STATUS 0x70 #define VIRTIO_REG_DEVICE_CONFIG 0x100 #define VIRTIO_STATUS_ACK 1 #define VIRTIO_STATUS_DRIVER 2 #define VIRTIO_STATUS_DRIVER_OK 4 #define VIRTIO_STATUS_FEAT_OK 8 #define VIRTQ_DESC_F_NEXT 1 #define VIRTQ_DESC_F_WRITE 2 #define VIRTQ_AVAIL_F_NO_INTERRUPT 1 #define VIRTIO_BLK_T_IN 0 #define VIRTIO_BLK_T_OUT 1 // Virtqueue structures struct virtq_desc { uint64_t addr; uint32_t len; uint16_t flags; uint16_t next; } __attribute__((packed)); struct virtq_avail { uint16_t flags; uint16_t index; uint16_t ring[VIRTQ_ENTRY_NUM]; } __attribute__((packed)); struct virtq_used_elem { uint32_t id; uint32_t len; } __attribute__((packed)); struct virtq_used { uint16_t flags; uint16_t index; struct virtq_used_elem ring[VIRTQ_ENTRY_NUM]; } __attribute__((packed)); struct virtio_virtq { struct virtq_desc descs[VIRTQ_ENTRY_NUM]; struct virtq_avail avail; struct virtq_used used __attribute__((aligned(PAGE_SIZE))); int queue_index; volatile uint16_t *used_index; uint16_t last_used_index; } __attribute__((packed)); struct virtio_blk_req { uint32_t type; uint32_t reserved; uint64_t sector; uint8_t data[512]; uint8_t status; } __attribute__((packed)); // File system #define FILES_MAX 2 #define DISK_MAX_SIZE align_up(sizeof(struct file) * FILES_MAX, SECTOR_SIZE) struct tar_header { char name[100]; char mode[8]; char uid[8]; char gid[8]; char size[12]; char mtime[12]; char checksum[8]; char type; char linkname[100]; char magic[6]; char version[2]; char uname[32]; char gname[32]; char devmajor[8]; char devminor[8]; char prefix[155]; char padding[12]; char data[]; // flexible array for file content } __attribute__((packed)); struct file { bool in_use; char name[100]; char data[1024]; size_t size; }; // Prototypes struct sbiret sbi_call(long arg0, long arg1, long arg2, long arg3, long arg4, long arg5, long fid, long eid); void putchar(char ch); void kernel_main(void);

kernel.ld

ENTRY(boot) SECTIONS { . = 0x80200000; __kernel_base = .; .text :{ KEEP(*(.text.boot)); *(.text .text.*); } .rodata : ALIGN(4) { *(.rodata .rodata.*); } .data : ALIGN(4) { *(.data .data.*); } .bss : ALIGN(4) { __bss = .; *(.bss .bss.* .sbss .sbss.*); __bss_end = .; } . = ALIGN(4); . += 128 * 1024; /* 128KB */ __stack_top = .; . = ALIGN(4096); __free_ram = .; . += 64 * 1024 * 1024; /* 64MB */ __free_ram_end = .; }

commom.c

#include "common.h" void putchar(char ch); // forward declaration void printf(const char *fmt, ...) { va_list vargs; va_start(vargs, fmt); while (*fmt) { if (*fmt == '%') { fmt++; // Skip '%' switch (*fmt) { case '\0': // '%' at the end putchar('%'); goto end; case '%': putchar('%'); break; case 's': { const char *s = va_arg(vargs, const char *); while (*s) { putchar(*s++); } break; } case 'd': { int value = va_arg(vargs, int); if (value < 0) { putchar('-'); value = -value; } int divisor = 1; while (value / divisor > 9) divisor *= 10; while (divisor > 0) { putchar('0' + value / divisor); value %= divisor; divisor /= 10; } break; } case 'x': { int value = va_arg(vargs, int); for (int i = 7; i >= 0; i--) { int nibble = (value >> (i * 4)) & 0xf; putchar("0123456789abcdef"[nibble]); } break; } } } else { putchar(*fmt); } fmt++; } end: va_end(vargs); } void *memcpy(void *dst, const void *src, size_t n) { uint8_t *d = (uint8_t *) dst; const uint8_t *s = (const uint8_t *) src; while (n--) { *d++ = *s++; } return dst; } void *memset(void *buf, char c, size_t n) { uint8_t *p = (uint8_t *) buf; while (n--) { *p++ = c; } return buf; } char *strcpy(char *dst, const char *src) { char *d = dst; while (*src) { *d++ = *src++; } *d = '\0'; return dst; } int strcmp(const char *s1, const char *s2) { while (*s1 && *s2) { if (*s1 != *s2) break; s1++; s2++; } return (unsigned char)*s1 - (unsigned char)*s2; }

common.h

#pragma once #define va_list __builtin_va_list #define va_start __builtin_va_start #define va_end __builtin_va_end #define va_arg __builtin_va_arg typedef int bool; typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef unsigned long long uint64_t; typedef uint32_t size_t; typedef uint32_t paddr_t; typedef uint32_t vaddr_t; #define true 1 #define false 0 #define NULL ((void *) 0) #define align_up(value, align) __builtin_align_up(value, align) #define is_aligned(value, align) __builtin_is_aligned(value, align) #define offsetof(type, member) __builtin_offsetof(type, member) void *memset(void *buf, char c, size_t n); void *memcpy(void *dst, const void *src, size_t n); char *strcpy(char *dst, const char *src); int strcmp(const char *s1, const char *s2); void printf(const char *fmt, ...); #define PAGE_SIZE 4096 // Syscall IDs #define SYS_GETCHAR 2 #define SYS_PUTCHAR 1 #define SYS_EXIT 3 #define SYS_READFILE 4 #define SYS_WRITEFILE 5

shell.c

#include "user.h" void main(void) { while (1) { prompt: printf("> "); char cmdline[128]; int i = 0; for (;;) { char ch = getchar(); // echo if (ch >= 0) { putchar(ch); } // stop if length exceeded if (i >= (int)sizeof(cmdline) - 1) { printf("\ncommand line too long\n"); goto prompt; } // check line endings if (ch == '\r' || ch == '\n') { putchar('\n'); cmdline[i] = '\0'; break; } else { cmdline[i++] = ch; } } if (!strcmp(cmdline, "hello")) { printf("Hello world from shell!\n"); } else if (!strcmp(cmdline, "exit")) { exit(); } else if (!strcmp(cmdline, "readfile")) { char buf[128]; int len = readfile("hello.txt", buf, sizeof(buf) - 1); if (len >= 0) { buf[len] = '\0'; printf("%s\n", buf); } } else if (!strcmp(cmdline, "writefile")) { const char *msg = "Hello from shell!\n"; int written = writefile("hello.txt", msg, 19); if (written >= 0) { printf("wrote %d bytes\n", written); } } else { printf("unknown command: %s\n", cmdline); } } }

user.c

#include "user.h" int syscall(int sysno, int arg0, int arg1, int arg2); extern char __stack_top[]; __attribute__((noreturn)) void exit(void) { syscall(SYS_EXIT, 0, 0, 0); for (;;); } void putchar(char ch) { syscall(SYS_PUTCHAR, ch, 0, 0); } __attribute__((section(".text.start"))) __attribute__((naked)) void start(void) { __asm__ __volatile__( "mv sp, %[stack_top]\n" "call main\n" "call exit\n" :: [stack_top] "r"(__stack_top) ); } int syscall(int sysno, int arg0, int arg1, int arg2) { register int a0 __asm__("a0") = arg0; register int a1 __asm__("a1") = arg1; register int a2 __asm__("a2") = arg2; register int a3 __asm__("a3") = sysno; __asm__ __volatile__("ecall" : "=r"(a0) : "r"(a0), "r"(a1), "r"(a2), "r"(a3) : "memory"); return a0; } int getchar(void) { return syscall(SYS_GETCHAR, 0, 0, 0); } int readfile(const char *filename, char *buf, int len) { return syscall(SYS_READFILE, (int)filename, (int)buf, len); } int writefile(const char *filename, const char *buf, int len) { return syscall(SYS_WRITEFILE, (int)filename, (int)buf, len); }

user.h

#pragma once #include "common.h" __attribute__((noreturn)) void exit(void); void putchar(char ch); int getchar(void); int readfile(const char *filename, char *buf, int len); int writefile(const char *filename, const char *buf, int len);

user.ld

ENTRY(start) SECTIONS { . = 0x1000000; /* machine code */ .text :{ KEEP(*(.text.start)); *(.text .text.*); } /* read-only data */ .rodata : ALIGN(4) { *(.rodata .rodata.*); } /* data with initial values */ .data : ALIGN(4) { *(.data .data.*); } /* data that should be zero-filled at startup */ .bss : ALIGN(4) { *(.bss .bss.* .sbss .sbss.*); . = ALIGN(16); . += 64 * 1024; /* 64KB */ __stack_top = .; ASSERT(. < 0x1800000, "too large executable"); } }

run.sh

#!/bin/bash set -xue QEMU=qemu-system-riscv32 CC=clang CFLAGS="-std=c11 -O2 -g3 -Wall -Wextra --target=riscv32 -ffreestanding -nostdlib" OBJCOPY=/usr/bin/llvm-objcopy # 1) Build user $CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf \ shell.c user.c common.c $OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin $OBJCOPY -Ibinary -Oelf32-littleriscv shell.bin shell.bin.o # 2) Build kernel $CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf \ kernel.c common.c shell.bin.o # 3) If there's a "disk" folder with *.txt, create disk.tar if [ -d disk ]; then (cd disk && tar cf ../disk.tar --format=ustar ./*.txt) fi # 4) Run QEMU using disk.tar as raw block device $QEMU -machine virt -bios default -nographic -serial mon:stdio --no-reboot \ -d unimp,guest_errors,int,cpu_reset -D qemu.log \ -drive id=drive0,file=disk.tar,format=raw,if=none \ -device virtio-blk-device,drive=drive0,bus=virtio-mmio-bus.0 \ -kernel kernel.elf

RESULT OF EXECUTION

螢幕擷取畫面 2025-01-21 040832
螢幕擷取畫面 2025-01-21 040839

Part II thread

In this part, I added the file containing the thread functions and adjusted several parts of the code. However, some errors occurred, which prevented me from testing the thread functions. I will show all the code in this section, explain the errors I encountered, and describe the steps I took to try to fix them.

pthread.c

#include "pthread.h" #include "common.h" #include "user.h" int pthread_create(pthread_t *tid, void (*fn)(void*), void* arg) { int r = syscall(SYS_THREAD_CREATE, (int)fn, (int)arg, 0); if(tid) *tid=r; return r; } void pthread_exit(void) { syscall(SYS_THREAD_EXIT,0,0,0); while(1){} } void pthread_yield(void) { syscall(SYS_THREAD_YIELD,0,0,0); }

pthread.h

#pragma once typedef int pthread_t; int pthread_create(pthread_t *tid, void (*start_routine)(void*), void *arg); void pthread_exit(void); void pthread_yield(void);

test-thread.c

#include "common.h" // #include "user.h" #include "pthread.h" typedef unsigned int uintptr_t; void worker(void *arg) { int id=(int)(uintptr_t)arg; for(int i=0;i<3;i++){ printf1("worker %d count=%d\n",id,i); pthread_yield(); } pthread_exit(); } void test_thread(void) { pthread_t t1,t2; printf1("Start threading test\n"); pthread_create(&t1, worker, (void*)1); pthread_create(&t2, worker, (void*)2); for(int i=0;i<5;i++){ printf1("main thread i=%d\n", i); pthread_yield(); } printf1("main done.\n"); }

common.c

#include "common.h" // forward putchar void putchar(char c); void printf(const char *fmt, ...) { va_list args; va_start(args, fmt); while(*fmt) { if(*fmt=='%'){ fmt++; switch(*fmt){ case 's': { const char* s = va_arg(args, const char*); while(*s){ putchar(*s++); } } break; case 'd': { int val = va_arg(args, int); if(val<0){ putchar('-'); val=-val; } int div=1; while(val/div>=10) div*=10; while(div>0){ putchar('0'+ (val/div)); val%=div; div/=10; } } break; case 'x': { unsigned v=va_arg(args,unsigned); for(int i=0;i<8;i++){ int shift=28 - i*4; unsigned nib = (v>>shift)&0xf; putchar("0123456789abcdef"[nib]); } } break; case '%': putchar('%'); break; default: putchar('%'); putchar(*fmt); } } else { putchar(*fmt); } fmt++; } va_end(args); } void *memcpy(void *dst, const void *src, size_t n) { uint8_t* d=(uint8_t*)dst; const uint8_t* s=(const uint8_t*)src; while(n--){ *d++ = *s++; } return dst; } void *memset(void *dst, char c, size_t n) { uint8_t* p=(uint8_t*)dst; while(n--){ *p++=c; } return dst; } int strcmp(const char*s1, const char*s2){ while(*s1 && *s2){ if(*s1!=*s2) break; s1++; s2++; } return (unsigned char)*s1 - (unsigned char)*s2; } char* strcpy(char*dst, const char*src){ char* o=dst; while((*dst++ = *src++)); return o; }

common.h

#pragma once #define va_list __builtin_va_list #define va_start __builtin_va_start #define va_end __builtin_va_end #define va_arg __builtin_va_arg typedef int bool; typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef unsigned long long uint64_t; typedef uint32_t size_t; typedef uint32_t paddr_t; typedef uint32_t vaddr_t; #define true 1 #define false 0 #define NULL ((void*)0) #define align_up(v,a) __builtin_align_up(v,a) #define is_aligned(v,a) __builtin_is_aligned(v,a) #define offsetof(type,member) __builtin_offsetof(type,member) #define PAGE_SIZE 4096 // Syscalls #define SYS_GETCHAR 2 #define SYS_PUTCHAR 1 #define SYS_EXIT 3 #define SYS_READFILE 4 #define SYS_WRITEFILE 5 #define SYS_THREAD_CREATE 6 #define SYS_THREAD_EXIT 7 #define SYS_THREAD_YIELD 8 void printf(const char *fmt, ...); void putchar(char c); void *memcpy(void *dst, const void *src, size_t n); void *memset(void *dst, char c, size_t n); int strcmp(const char*, const char*); char* strcpy(char*, const char*);

version I kernel.c

#include "kernel.h" #include "common.h" // Basic type definitions typedef unsigned char uint8_t; typedef unsigned int uint32_t; typedef uint32_t size_t; // BSS symbols from linker script extern char __bss[], __bss_end[], __stack_top[]; // Forward declarations void yield(void); void user_entry(void); void map_page(uint32_t *table, uint32_t vaddr, paddr_t paddr, uint32_t flags); void handle_syscall(struct trap_frame *f); long getchar(void); void fs_init(void); extern struct file *fs_lookup(const char *filename); extern struct process* kernel_create_thread(uint32_t func, uint32_t arg); void fs_flush(void); void read_write_disk(void *buf, unsigned sector, int is_write); struct virtio_virtq *virtq_init(unsigned index); // We place the idle process and the current process pointers here struct process *current_proc; struct process *idle_proc; // The main entry from the linker script __attribute__((section(".text.boot"))) __attribute__((naked)) void boot(void) { __asm__ __volatile__( "mv sp, %[stack_top]\n" "j kernel_main\n" : : [stack_top] "r"(__stack_top) ); } // SBI call struct sbiret sbi_call(long arg0, long arg1, long arg2, long arg3, long arg4, long arg5, long fid, long eid) { register long a0 __asm__("a0") = arg0; register long a1 __asm__("a1") = arg1; register long a2 __asm__("a2") = arg2; register long a3 __asm__("a3") = arg3; register long a4 __asm__("a4") = arg4; register long a5 __asm__("a5") = arg5; register long a6 __asm__("a6") = fid; register long a7 __asm__("a7") = eid; __asm__ __volatile__("ecall" : "=r"(a0), "=r"(a1) : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(a6), "r"(a7) : "memory"); return (struct sbiret){.error = a0, .value = a1}; } // Write a character using SBI void putchar(char ch) { sbi_call(ch, 0, 0, 0, 0, 0, 0, 1 /* SBI console putchar */); } // The low-level trap entry __attribute__((naked)) __attribute__((aligned(4))) void kernel_entry(void) { __asm__ __volatile__( "csrw sscratch, sp\n" "addi sp, sp, -4*31\n" "sw ra, 4*0(sp)\n" "sw gp, 4*1(sp)\n" "sw tp, 4*2(sp)\n" "sw t0, 4*3(sp)\n" "sw t1, 4*4(sp)\n" "sw t2, 4*5(sp)\n" "sw t3, 4*6(sp)\n" "sw t4, 4*7(sp)\n" "sw t5, 4*8(sp)\n" "sw t6, 4*9(sp)\n" "sw a0, 4*10(sp)\n" "sw a1, 4*11(sp)\n" "sw a2, 4*12(sp)\n" "sw a3, 4*13(sp)\n" "sw a4, 4*14(sp)\n" "sw a5, 4*15(sp)\n" "sw a6, 4*16(sp)\n" "sw a7, 4*17(sp)\n" "sw s0, 4*18(sp)\n" "sw s1, 4*19(sp)\n" "sw s2, 4*20(sp)\n" "sw s3, 4*21(sp)\n" "sw s4, 4*22(sp)\n" "sw s5, 4*23(sp)\n" "sw s6, 4*24(sp)\n" "sw s7, 4*25(sp)\n" "sw s8, 4*26(sp)\n" "sw s9, 4*27(sp)\n" "sw s10, 4*28(sp)\n" "sw s11, 4*29(sp)\n" "csrr a0, sscratch\n" "sw a0, 4*30(sp)\n" // Reset stack "addi a0, sp, 4*31\n" "csrw sscratch, a0\n" "mv a0, sp\n" "call handle_trap\n" // restore registers "lw ra, 4*0(sp)\n" "lw gp, 4*1(sp)\n" "lw tp, 4*2(sp)\n" "lw t0, 4*3(sp)\n" "lw t1, 4*4(sp)\n" "lw t2, 4*5(sp)\n" "lw t3, 4*6(sp)\n" "lw t4, 4*7(sp)\n" "lw t5, 4*8(sp)\n" "lw t6, 4*9(sp)\n" "lw a0, 4*10(sp)\n" "lw a1, 4*11(sp)\n" "lw a2, 4*12(sp)\n" "lw a3, 4*13(sp)\n" "lw a4, 4*14(sp)\n" "lw a5, 4*15(sp)\n" "lw a6, 4*16(sp)\n" "lw a7, 4*17(sp)\n" "lw s0, 4*18(sp)\n" "lw s1, 4*19(sp)\n" "lw s2, 4*20(sp)\n" "lw s3, 4*21(sp)\n" "lw s4, 4*22(sp)\n" "lw s5, 4*23(sp)\n" "lw s6, 4*24(sp)\n" "lw s7, 4*25(sp)\n" "lw s8, 4*26(sp)\n" "lw s9, 4*27(sp)\n" "lw s10, 4*28(sp)\n" "lw s11, 4*29(sp)\n" "lw sp, 4*30(sp)\n" "sret\n" ); } void handle_trap(struct trap_frame *f) { uint32_t scause = READ_CSR(scause); uint32_t stval = READ_CSR(stval); uint32_t user_pc= READ_CSR(sepc); if (scause == SCAUSE_ECALL) { handle_syscall(f); user_pc += 4; } else { PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc); } WRITE_CSR(sepc, user_pc); } extern char __free_ram[], __free_ram_end[]; // Simple page allocator paddr_t alloc_pages(uint32_t n) { static paddr_t next_paddr = (paddr_t)__free_ram; paddr_t paddr = next_paddr; next_paddr += n * PAGE_SIZE; if (next_paddr > (paddr_t)__free_ram_end) { PANIC("out of memory"); } memset((void *)paddr, 0, n * PAGE_SIZE); return paddr; } // Switch context __attribute__((naked)) void switch_context(uint32_t *prev_sp, uint32_t *next_sp) { __asm__ __volatile__( "addi sp, sp, -13 * 4\n" "sw ra, 0 * 4(sp)\n" "sw s0, 1 * 4(sp)\n" "sw s1, 2 * 4(sp)\n" "sw s2, 3 * 4(sp)\n" "sw s3, 4 * 4(sp)\n" "sw s4, 5 * 4(sp)\n" "sw s5, 6 * 4(sp)\n" "sw s6, 7 * 4(sp)\n" "sw s7, 8 * 4(sp)\n" "sw s8, 9 * 4(sp)\n" "sw s9, 10 * 4(sp)\n" "sw s10, 11 * 4(sp)\n" "sw s11, 12 * 4(sp)\n" "sw sp, (a0)\n" // save old sp "lw sp, (a1)\n" // load new sp "lw ra, 0 * 4(sp)\n" "lw s0, 1 * 4(sp)\n" "lw s1, 2 * 4(sp)\n" "lw s2, 3 * 4(sp)\n" "lw s3, 4 * 4(sp)\n" "lw s4, 5 * 4(sp)\n" "lw s5, 6 * 4(sp)\n" "lw s6, 7 * 4(sp)\n" "lw s7, 8 * 4(sp)\n" "lw s8, 9 * 4(sp)\n" "lw s9, 10 * 4(sp)\n" "lw s10, 11 * 4(sp)\n" "lw s11, 12 * 4(sp)\n" "addi sp, sp, 13 * 4\n" "ret\n" ); } // Process definitions #define PROCS_MAX 8 #define PROC_UNUSED 0 #define PROC_RUNNABLE 1 #define PROC_EXITED 2 /* struct process layout: offset 0..3: pid offset 4..7: state offset 8..11: sp offset 12..15: page_table offset 16..35: dummy (20 bytes) offset 36..39: entry offset 40..43: arg offset 44..: stack */ // This is the process struct struct process { int pid; int state; vaddr_t sp; uint32_t *page_table; uint8_t dummy[20]; uint32_t entry; uint32_t arg; uint8_t stack[8192]; }; extern char __kernel_base[]; // Global processes array struct process procs[PROCS_MAX]; void map_page(uint32_t *table, uint32_t vaddr, paddr_t paddr, uint32_t flags); // Create a process from an ELF/image struct process* create_process(const void *image, size_t image_size) { struct process *proc = NULL; for (int i = 0; i < PROCS_MAX; i++) { if (procs[i].state == PROC_UNUSED) { proc = &procs[i]; break; } } if (!proc) { PANIC("no free process slots"); } memset(proc, 0, sizeof(*proc)); // set up stack with callee-saved regs uint32_t *sp = (uint32_t *)&proc->stack[8192]; for (int r = 0; r < 12; r++) { *--sp = 0; } // set return address to user_entry *--sp = (uint32_t)user_entry; // allocate page table uint32_t *page_table = (uint32_t *)alloc_pages(1); // map the kernel memory for (paddr_t paddr = (paddr_t)__kernel_base; paddr < (paddr_t)__free_ram_end; paddr += PAGE_SIZE) { map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X); } // map virtio-blk map_page(page_table, VIRTIO_BLK_PADDR, VIRTIO_BLK_PADDR, PAGE_R | PAGE_W); proc->pid = 1; proc->state = PROC_RUNNABLE; proc->sp = (uint32_t)sp; proc->page_table = page_table; // load user image if size > 0 uint32_t off = 0; while (off < image_size) { paddr_t pg = alloc_pages(1); size_t remaining = image_size - off; size_t cpy_size = (remaining > PAGE_SIZE) ? PAGE_SIZE : remaining; memcpy((void*)pg, (const uint8_t*)image + off, cpy_size); map_page(page_table, USER_BASE + off, pg, PAGE_U | PAGE_R | PAGE_W | PAGE_X); off += PAGE_SIZE; } return proc; } void yield(void) { struct process *next = idle_proc; for (int i = 0; i < PROCS_MAX; i++) { int idx = (current_proc->pid + i) % PROCS_MAX; struct process *p = &procs[idx]; if (p->state == PROC_RUNNABLE && p->pid > 0) { next = p; break; } } if (next == current_proc) { return; } __asm__ __volatile__( "sfence.vma\n" "csrw satp, %[satp]\n" "sfence.vma\n" "csrw sscratch, %[sscratch]\n" : : [satp] "r"(SATP_SV32 | ((uint32_t)next->page_table / PAGE_SIZE)), [sscratch] "r"((uint32_t)&next->stack[8192]) ); struct process *prev = current_proc; current_proc = next; switch_context(&prev->sp, &next->sp); } void map_page(uint32_t *table1, uint32_t vaddr, paddr_t paddr, uint32_t flags) { if (!is_aligned(vaddr, PAGE_SIZE)) { PANIC("unaligned vaddr"); } if (!is_aligned(paddr, PAGE_SIZE)) { PANIC("unaligned paddr"); } uint32_t vpn1 = (vaddr >> 22) & 0x3ff; if ((table1[vpn1] & PAGE_V) == 0) { uint32_t pt_paddr = alloc_pages(1); table1[vpn1] = ((pt_paddr / PAGE_SIZE) << 10) | PAGE_V; } uint32_t vpn0 = (vaddr >> 12) & 0x3ff; uint32_t *table0 = (uint32_t *)((table1[vpn1] >> 10) * PAGE_SIZE); table0[vpn0] = ((paddr / PAGE_SIZE) << 10) | (flags | PAGE_V); } // The user entry code __attribute__((naked)) void user_entry(void) { __asm__ __volatile__( "csrw sepc, %[sepc]\n" "csrw sstatus, %[sstatus]\n" "sret\n" : : [sepc] "r"(USER_BASE), [sstatus] "r"(SSTATUS_SPIE | SSTATUS_SUM) ); } __attribute__((naked)) void thread_trampoline(void) { __asm__ __volatile__( "la a0, current_proc\n" "lw a0, 0(a0)\n" "lw t0, 36(a0)\n" "lw t1, 40(a0)\n" "mv a0, t1\n" "jalr t0\n" "li a0, 0\n" "li a7, 7\n" "ecall\n" ); } struct process* kernel_create_thread(uint32_t func, uint32_t arg) { struct process* proc = NULL; for (int i = 0; i < PROCS_MAX; i++) { if (procs[i].state == PROC_UNUSED) { proc = &procs[i]; break; } } if (!proc) { PANIC("no free thread slots"); } memset(proc, 0, sizeof(*proc)); uint32_t *sp = (uint32_t*)&proc->stack[8192]; for (int r = 0; r < 12; r++) { *--sp = 0; } extern void thread_trampoline(void); *--sp = (uint32_t)thread_trampoline; proc->entry = func; proc->arg = arg; uint32_t *pgtable = (uint32_t*)alloc_pages(1); for (paddr_t p = (paddr_t)__kernel_base; p < (paddr_t)__free_ram_end; p += PAGE_SIZE) { map_page(pgtable, p, p, PAGE_R|PAGE_W|PAGE_X); } map_page(pgtable, VIRTIO_BLK_PADDR, VIRTIO_BLK_PADDR, PAGE_R|PAGE_W); proc->pid = 100; proc->state = PROC_RUNNABLE; proc->sp = (uint32_t)sp; proc->page_table = pgtable; return proc; } void handle_syscall(struct trap_frame *f) { switch (f->a3) { case SYS_GETCHAR: while (1) { long ch = getchar(); if (ch >= 0) { f->a0 = ch; break; } yield(); } break; case SYS_EXIT: printf("process %d exited\n", current_proc->pid); current_proc->state = PROC_EXITED; yield(); while (1) {} case SYS_PUTCHAR: putchar(f->a0); break; case SYS_READFILE: case SYS_WRITEFILE: { const char *filename = (const char *)f->a0; char *buf = (char *)f->a1; int len = f->a2; struct file *file = fs_lookup(filename); if (!file) { printf("file not found: %s\n", filename); f->a0 = -1; break; } if (f->a3 == SYS_READFILE) { if (len > (int)file->size) { len = file->size; } memcpy(buf, file->data, len); f->a0 = len; } else { if (len > (int)sizeof(file->data)) { len = (int)sizeof(file->data); } memcpy(file->data, buf, len); file->size = len; fs_flush(); f->a0 = len; } } break; case SYS_THREAD_CREATE: { uint32_t ret = (uint32_t)kernel_create_thread((uint32_t)f->a0,(uint32_t)f->a1); f->a0 = ret; } break; case SYS_THREAD_EXIT: current_proc->state = PROC_EXITED; yield(); while (1) {} case SYS_THREAD_YIELD: yield(); break; default: PANIC("unexpected syscall a3=%x\n", f->a3); } } long getchar(void) { struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2); return ret.error; } // Virtio-blk definitions struct virtio_virtq *blk_request_vq; struct virtio_blk_req *blk_req; paddr_t blk_req_paddr; unsigned blk_capacity; uint32_t virtio_reg_read32(unsigned offset) { return *((volatile uint32_t*)(VIRTIO_BLK_PADDR + offset)); } // read 64 bits by two 32-bit reads uint64_t virtio_reg_read64(unsigned offset) { uint32_t low = virtio_reg_read32(offset); uint32_t high = virtio_reg_read32(offset + 4); return ((uint64_t)high << 32) | (uint64_t)low; } void virtio_reg_write32(unsigned offset, uint32_t value) { *((volatile uint32_t*)(VIRTIO_BLK_PADDR + offset)) = value; } void virtio_reg_fetch_and_or32(unsigned offset, uint32_t value) { uint32_t old = virtio_reg_read32(offset); virtio_reg_write32(offset, old | value); } struct virtio_virtq* virtq_init(unsigned index); // Initialize virtio-blk void virtio_blk_init(void) { if (virtio_reg_read32(VIRTIO_REG_MAGIC) != 0x74726976) PANIC("virtio: invalid magic"); if (virtio_reg_read32(VIRTIO_REG_VERSION) != 1) PANIC("virtio: invalid version"); if (virtio_reg_read32(VIRTIO_REG_DEVICE_ID) != VIRTIO_DEVICE_BLK) PANIC("virtio: invalid device id"); virtio_reg_write32(VIRTIO_REG_DEVICE_STATUS, 0); virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_ACK); virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_DRIVER); virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_FEAT_OK); blk_request_vq = virtq_init(0); virtio_reg_write32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_DRIVER_OK); blk_capacity = virtio_reg_read64(VIRTIO_REG_DEVICE_CONFIG) * SECTOR_SIZE; printf("virtio-blk: capacity is %d bytes\n", blk_capacity); blk_req_paddr = alloc_pages(1); blk_req = (struct virtio_blk_req*)blk_req_paddr; } // The virtqueue structure struct virtio_virtq { struct virtq_desc descs[VIRTQ_ENTRY_NUM]; struct virtq_avail avail; struct virtq_used used __attribute__((aligned(PAGE_SIZE))); int queue_index; volatile uint16_t *used_index; uint16_t last_used_index; }; struct virtio_virtq* virtq_init(unsigned index) { paddr_t vq_paddr = alloc_pages(1); struct virtio_virtq *vq = (struct virtio_virtq*)vq_paddr; vq->queue_index = index; vq->used_index = &vq->used.index; virtio_reg_write32(VIRTIO_REG_QUEUE_SEL, index); virtio_reg_write32(VIRTIO_REG_QUEUE_NUM, VIRTQ_ENTRY_NUM); virtio_reg_write32(VIRTIO_REG_QUEUE_ALIGN, 0); virtio_reg_write32(VIRTIO_REG_QUEUE_PFN, vq_paddr); return vq; } void virtq_kick(struct virtio_virtq *vq, int desc_index) { vq->avail.ring[vq->avail.index % VIRTQ_ENTRY_NUM] = desc_index; vq->avail.index++; __sync_synchronize(); virtio_reg_write32(VIRTIO_REG_QUEUE_NOTIFY, vq->queue_index); vq->last_used_index++; } bool virtq_is_busy(struct virtio_virtq *vq) { return vq->last_used_index != *vq->used_index; } // read/write disk void read_write_disk(void *buf, unsigned sector, int is_write) { if (sector >= blk_capacity / SECTOR_SIZE) { printf("virtio: tried to read/write sector=%d, but capacity=%d\n", sector, blk_capacity / SECTOR_SIZE); return; } blk_req->sector = sector; blk_req->type = is_write ? VIRTIO_BLK_T_OUT : VIRTIO_BLK_T_IN; if (is_write) { memcpy(blk_req->data, buf, SECTOR_SIZE); } struct virtio_virtq *vq = blk_request_vq; vq->descs[0].addr = blk_req_paddr; vq->descs[0].len = 4 * 2 + 8; vq->descs[0].flags = VIRTQ_DESC_F_NEXT; vq->descs[0].next = 1; vq->descs[1].addr = blk_req_paddr + offsetof(struct virtio_blk_req, data); vq->descs[1].len = SECTOR_SIZE; vq->descs[1].flags = VIRTQ_DESC_F_NEXT | (is_write ? 0 : VIRTQ_DESC_F_WRITE); vq->descs[1].next = 2; vq->descs[2].addr = blk_req_paddr + offsetof(struct virtio_blk_req, status); vq->descs[2].len = 1; vq->descs[2].flags = VIRTQ_DESC_F_WRITE; virtq_kick(vq, 0); while (virtq_is_busy(vq)) {} if (blk_req->status != 0) { printf("virtio: warn: read/write sector=%d status=%d\n", sector, blk_req->status); return; } if (!is_write) { memcpy(buf, blk_req->data, SECTOR_SIZE); } } // File system structures struct file files[FILES_MAX]; uint8_t disk[DISK_MAX_SIZE]; // Utility to parse oct string int oct2int(char *oct, int len) { int dec = 0; for (int i = 0; i < len; i++) { if (oct[i] < '0' || oct[i] > '7') break; dec = dec * 8 + (oct[i] - '0'); } return dec; } void fs_init(void) { for (unsigned sector = 0; sector < sizeof(disk) / SECTOR_SIZE; sector++) { read_write_disk(&disk[sector * SECTOR_SIZE], sector, 0); } unsigned off = 0; for (int i = 0; i < FILES_MAX; i++) { struct tar_header *hdr = (struct tar_header *)&disk[off]; if (hdr->name[0] == '\0') break; if (strcmp(hdr->magic, "ustar") != 0) { PANIC("invalid tar header"); } int sz = oct2int(hdr->size, sizeof(hdr->size)); char *np = hdr->name; if (np[0] == '.' && np[1] == '/') { np += 2; } struct file *f = &files[i]; f->in_use = true; strcpy(f->name, np); memcpy(f->data, hdr->data, sz); f->size = sz; printf("file: %s, size=%d\n", f->name, f->size); off += align_up(sizeof(struct tar_header) + sz, SECTOR_SIZE); if (off >= sizeof(disk)) break; } } void fs_flush(void) { memset(disk, 0, sizeof(disk)); unsigned off = 0; for (int i = 0; i < FILES_MAX; i++) { struct file *f = &files[i]; if (!f->in_use) continue; struct tar_header *hdr = (struct tar_header *)&disk[off]; memset(hdr, 0, sizeof(*hdr)); strcpy(hdr->name, f->name); strcpy(hdr->mode, "000644"); strcpy(hdr->magic, "ustar"); strcpy(hdr->version, "00"); hdr->type = '0'; int sz = f->size; for (int j = sizeof(hdr->size); j > 0; j--) { hdr->size[j - 1] = (sz % 8) + '0'; sz /= 8; } int csum = ' ' * sizeof(hdr->checksum); for (unsigned k = 0; k < sizeof(struct tar_header); k++) { csum += (unsigned char)disk[off + k]; } for (int j = 5; j >= 0; j--) { hdr->checksum[j] = (csum % 8) + '0'; csum /= 8; } memcpy(hdr->data, f->data, f->size); off += align_up(sizeof(struct tar_header) + f->size, SECTOR_SIZE); if (off >= sizeof(disk)) break; } for (unsigned sec = 0; sec < sizeof(disk) / SECTOR_SIZE; sec++) { read_write_disk(&disk[sec * SECTOR_SIZE], sec, 1); } printf("wrote %d bytes to disk\n", (int)sizeof(disk)); } struct file *fs_lookup(const char *filename) { for (int i = 0; i < FILES_MAX; i++) { if (!strcmp(files[i].name, filename)) { return &files[i]; } } return NULL; } // these come from shell.bin.o extern char _binary_shell_bin_start[]; extern char _binary_shell_bin_size[]; void kernel_main(void) { // Clear BSS memset(__bss, 0, (size_t)__bss_end - (size_t)__bss); printf("\n\n"); WRITE_CSR(stvec, (uint32_t)kernel_entry); virtio_blk_init(); // Just test read sector 0 char tmp[SECTOR_SIZE]; // read_write_disk(tmp, 0, 0); printf("first sector: %s\n", tmp); fs_init(); // Create idle process struct process* idle = create_process(NULL, 0); idle->pid = -1; current_proc = idle_proc = idle; // Debug: check if shell.bin actually got linked printf("DEBUG: _binary_shell_bin_start=%x, _binary_shell_bin_size=%x\n", _binary_shell_bin_start, (unsigned)_binary_shell_bin_size); size_t shell_size = (size_t)_binary_shell_bin_size; if (shell_size > 0) { create_process(_binary_shell_bin_start, shell_size); printf("Created shell process!\n"); } else { printf("WARNING: shell.bin is empty.\n"); } // If there is a valid shell image, create it as a process: yield(); PANIC("switched to idle process"); while (1) { // The idle loop. If all processes exit, we might just hang here. } }

In the kernel.c file, I added functions related to threads and executed the program. However, the result was unexpected. To debug, I used printf statements to check how far the program ran. The output showed that the program encountered an error before even reaching kernel_main. Later, I used the LLVM tool to trace memory addresses and found that the issue seemed to originate from the disk-related code. As a result, I decided to comment out all disk-related parts temporarily, as I wanted to first test whether the thread functionality was working properly. So, I proceeded with this approach.
image

"version II kernel.c"

#include "kernel.h" #include "common.h" // Basic type definitions typedef unsigned char uint8_t; typedef unsigned int uint32_t; typedef uint32_t size_t; // BSS symbols from linker script extern char __bss[], __bss_end[], __stack_top[]; // Forward declarations void yield(void); void user_entry(void); void map_page(uint32_t *table, uint32_t vaddr, paddr_t paddr, uint32_t flags); void handle_syscall(struct trap_frame *f); long getchar(void); // void fs_init(void); extern struct file *fs_lookup(const char *filename); extern struct process* kernel_create_thread(uint32_t func, uint32_t arg); // void fs_flush(void); // void read_write_disk(void *buf, unsigned sector, int is_write); struct virtio_virtq *virtq_init(unsigned index); // We place the idle process and the current process pointers here struct process *current_proc; struct process *idle_proc; // The main entry from the linker script __attribute__((section(".text.boot"))) __attribute__((naked)) void boot(void) { __asm__ __volatile__( "mv sp, %[stack_top]\n" "j kernel_main\n" : : [stack_top] "r"(__stack_top) ); } // SBI call struct sbiret sbi_call(long arg0, long arg1, long arg2, long arg3, long arg4, long arg5, long fid, long eid) { register long a0 __asm__("a0") = arg0; register long a1 __asm__("a1") = arg1; register long a2 __asm__("a2") = arg2; register long a3 __asm__("a3") = arg3; register long a4 __asm__("a4") = arg4; register long a5 __asm__("a5") = arg5; register long a6 __asm__("a6") = fid; register long a7 __asm__("a7") = eid; __asm__ __volatile__("ecall" : "=r"(a0), "=r"(a1) : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(a6), "r"(a7) : "memory"); return (struct sbiret){.error = a0, .value = a1}; } // Write a character using SBI void putchar(char ch) { sbi_call(ch, 0, 0, 0, 0, 0, 0, 1 /* SBI console putchar */); } // The low-level trap entry __attribute__((naked)) __attribute__((aligned(4))) void kernel_entry(void) { __asm__ __volatile__( "csrw sscratch, sp\n" "addi sp, sp, -4*31\n" "sw ra, 4*0(sp)\n" "sw gp, 4*1(sp)\n" "sw tp, 4*2(sp)\n" "sw t0, 4*3(sp)\n" "sw t1, 4*4(sp)\n" "sw t2, 4*5(sp)\n" "sw t3, 4*6(sp)\n" "sw t4, 4*7(sp)\n" "sw t5, 4*8(sp)\n" "sw t6, 4*9(sp)\n" "sw a0, 4*10(sp)\n" "sw a1, 4*11(sp)\n" "sw a2, 4*12(sp)\n" "sw a3, 4*13(sp)\n" "sw a4, 4*14(sp)\n" "sw a5, 4*15(sp)\n" "sw a6, 4*16(sp)\n" "sw a7, 4*17(sp)\n" "sw s0, 4*18(sp)\n" "sw s1, 4*19(sp)\n" "sw s2, 4*20(sp)\n" "sw s3, 4*21(sp)\n" "sw s4, 4*22(sp)\n" "sw s5, 4*23(sp)\n" "sw s6, 4*24(sp)\n" "sw s7, 4*25(sp)\n" "sw s8, 4*26(sp)\n" "sw s9, 4*27(sp)\n" "sw s10, 4*28(sp)\n" "sw s11, 4*29(sp)\n" "csrr a0, sscratch\n" "sw a0, 4*30(sp)\n" // Reset stack "addi a0, sp, 4*31\n" "csrw sscratch, a0\n" "mv a0, sp\n" "call handle_trap\n" // restore registers "lw ra, 4*0(sp)\n" "lw gp, 4*1(sp)\n" "lw tp, 4*2(sp)\n" "lw t0, 4*3(sp)\n" "lw t1, 4*4(sp)\n" "lw t2, 4*5(sp)\n" "lw t3, 4*6(sp)\n" "lw t4, 4*7(sp)\n" "lw t5, 4*8(sp)\n" "lw t6, 4*9(sp)\n" "lw a0, 4*10(sp)\n" "lw a1, 4*11(sp)\n" "lw a2, 4*12(sp)\n" "lw a3, 4*13(sp)\n" "lw a4, 4*14(sp)\n" "lw a5, 4*15(sp)\n" "lw a6, 4*16(sp)\n" "lw a7, 4*17(sp)\n" "lw s0, 4*18(sp)\n" "lw s1, 4*19(sp)\n" "lw s2, 4*20(sp)\n" "lw s3, 4*21(sp)\n" "lw s4, 4*22(sp)\n" "lw s5, 4*23(sp)\n" "lw s6, 4*24(sp)\n" "lw s7, 4*25(sp)\n" "lw s8, 4*26(sp)\n" "lw s9, 4*27(sp)\n" "lw s10, 4*28(sp)\n" "lw s11, 4*29(sp)\n" "lw sp, 4*30(sp)\n" "sret\n" ); } void handle_trap(struct trap_frame *f) { uint32_t scause = READ_CSR(scause); uint32_t stval = READ_CSR(stval); uint32_t user_pc= READ_CSR(sepc); if (scause == SCAUSE_ECALL) { handle_syscall(f); user_pc += 4; } else { PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc); } WRITE_CSR(sepc, user_pc); } extern char __free_ram[], __free_ram_end[]; // Simple page allocator paddr_t alloc_pages(uint32_t n) { static paddr_t next_paddr = (paddr_t)__free_ram; paddr_t paddr = next_paddr; next_paddr += n * PAGE_SIZE; if (next_paddr > (paddr_t)__free_ram_end) { PANIC("out of memory"); } memset((void *)paddr, 0, n * PAGE_SIZE); return paddr; } // Switch context __attribute__((naked)) void switch_context(uint32_t *prev_sp, uint32_t *next_sp) { __asm__ __volatile__( "addi sp, sp, -13 * 4\n" "sw ra, 0 * 4(sp)\n" "sw s0, 1 * 4(sp)\n" "sw s1, 2 * 4(sp)\n" "sw s2, 3 * 4(sp)\n" "sw s3, 4 * 4(sp)\n" "sw s4, 5 * 4(sp)\n" "sw s5, 6 * 4(sp)\n" "sw s6, 7 * 4(sp)\n" "sw s7, 8 * 4(sp)\n" "sw s8, 9 * 4(sp)\n" "sw s9, 10 * 4(sp)\n" "sw s10, 11 * 4(sp)\n" "sw s11, 12 * 4(sp)\n" "sw sp, (a0)\n" // save old sp "lw sp, (a1)\n" // load new sp "lw ra, 0 * 4(sp)\n" "lw s0, 1 * 4(sp)\n" "lw s1, 2 * 4(sp)\n" "lw s2, 3 * 4(sp)\n" "lw s3, 4 * 4(sp)\n" "lw s4, 5 * 4(sp)\n" "lw s5, 6 * 4(sp)\n" "lw s6, 7 * 4(sp)\n" "lw s7, 8 * 4(sp)\n" "lw s8, 9 * 4(sp)\n" "lw s9, 10 * 4(sp)\n" "lw s10, 11 * 4(sp)\n" "lw s11, 12 * 4(sp)\n" "addi sp, sp, 13 * 4\n" "ret\n" ); } // Process definitions #define PROCS_MAX 8 #define PROC_UNUSED 0 #define PROC_RUNNABLE 1 #define PROC_EXITED 2 /* struct process layout: offset 0..3: pid offset 4..7: state offset 8..11: sp offset 12..15: page_table offset 16..35: dummy (20 bytes) offset 36..39: entry offset 40..43: arg offset 44..: stack */ // This is the process struct struct process { int pid; int state; vaddr_t sp; uint32_t *page_table; uint8_t dummy[20]; uint32_t entry; uint32_t arg; uint8_t stack[8192]; }; extern char __kernel_base[]; // Global processes array struct process procs[PROCS_MAX]; void map_page(uint32_t *table, uint32_t vaddr, paddr_t paddr, uint32_t flags); // Create a process from an ELF/image struct process* create_process(const void *image, size_t image_size) { struct process *proc = NULL; for (int i = 0; i < PROCS_MAX; i++) { if (procs[i].state == PROC_UNUSED) { proc = &procs[i]; break; } } if (!proc) { PANIC("no free process slots"); } memset(proc, 0, sizeof(*proc)); // set up stack with callee-saved regs uint32_t *sp = (uint32_t *)&proc->stack[8192]; for (int r = 0; r < 12; r++) { *--sp = 0; } // set return address to user_entry *--sp = (uint32_t)user_entry; // allocate page table uint32_t *page_table = (uint32_t *)alloc_pages(1); // map the kernel memory for (paddr_t paddr = (paddr_t)__kernel_base; paddr < (paddr_t)__free_ram_end; paddr += PAGE_SIZE) { map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X); } // map virtio-blk map_page(page_table, VIRTIO_BLK_PADDR, VIRTIO_BLK_PADDR, PAGE_R | PAGE_W); proc->pid = 1; proc->state = PROC_RUNNABLE; proc->sp = (uint32_t)sp; proc->page_table = page_table; // load user image if size > 0 uint32_t off = 0; while (off < image_size) { paddr_t pg = alloc_pages(1); size_t remaining = image_size - off; size_t cpy_size = (remaining > PAGE_SIZE) ? PAGE_SIZE : remaining; memcpy((void*)pg, (const uint8_t*)image + off, cpy_size); map_page(page_table, USER_BASE + off, pg, PAGE_U | PAGE_R | PAGE_W | PAGE_X); off += PAGE_SIZE; } return proc; } void yield(void) { struct process *next = idle_proc; for (int i = 0; i < PROCS_MAX; i++) { int idx = (current_proc->pid + i) % PROCS_MAX; struct process *p = &procs[idx]; if (p->state == PROC_RUNNABLE && p->pid > 0) { next = p; break; } } if (next == current_proc) { return; } __asm__ __volatile__( "sfence.vma\n" "csrw satp, %[satp]\n" "sfence.vma\n" "csrw sscratch, %[sscratch]\n" : : [satp] "r"(SATP_SV32 | ((uint32_t)next->page_table / PAGE_SIZE)), [sscratch] "r"((uint32_t)&next->stack[8192]) ); struct process *prev = current_proc; current_proc = next; switch_context(&prev->sp, &next->sp); } void map_page(uint32_t *table1, uint32_t vaddr, paddr_t paddr, uint32_t flags) { if (!is_aligned(vaddr, PAGE_SIZE)) { PANIC("unaligned vaddr"); } if (!is_aligned(paddr, PAGE_SIZE)) { PANIC("unaligned paddr"); } uint32_t vpn1 = (vaddr >> 22) & 0x3ff; if ((table1[vpn1] & PAGE_V) == 0) { uint32_t pt_paddr = alloc_pages(1); table1[vpn1] = ((pt_paddr / PAGE_SIZE) << 10) | PAGE_V; } uint32_t vpn0 = (vaddr >> 12) & 0x3ff; uint32_t *table0 = (uint32_t *)((table1[vpn1] >> 10) * PAGE_SIZE); table0[vpn0] = ((paddr / PAGE_SIZE) << 10) | (flags | PAGE_V); } // The user entry code __attribute__((naked)) void user_entry(void) { __asm__ __volatile__( "csrw sepc, %[sepc]\n" "csrw sstatus, %[sstatus]\n" "sret\n" : : [sepc] "r"(USER_BASE), [sstatus] "r"(SSTATUS_SPIE | SSTATUS_SUM) ); } __attribute__((naked)) void thread_trampoline(void) { __asm__ __volatile__( "la a0, current_proc\n" "lw a0, 0(a0)\n" "lw t0, 36(a0)\n" "lw t1, 40(a0)\n" "mv a0, t1\n" "jalr t0\n" "li a0, 0\n" "li a7, 7\n" "ecall\n" ); } struct process* kernel_create_thread(uint32_t func, uint32_t arg) { struct process* proc = NULL; for (int i = 0; i < PROCS_MAX; i++) { if (procs[i].state == PROC_UNUSED) { proc = &procs[i]; break; } } if (!proc) { PANIC("no free thread slots"); } memset(proc, 0, sizeof(*proc)); uint32_t *sp = (uint32_t*)&proc->stack[8192]; for (int r = 0; r < 12; r++) { *--sp = 0; } extern void thread_trampoline(void); *--sp = (uint32_t)thread_trampoline; proc->entry = func; proc->arg = arg; uint32_t *pgtable = (uint32_t*)alloc_pages(1); for (paddr_t p = (paddr_t)__kernel_base; p < (paddr_t)__free_ram_end; p += PAGE_SIZE) { map_page(pgtable, p, p, PAGE_R|PAGE_W|PAGE_X); } map_page(pgtable, VIRTIO_BLK_PADDR, VIRTIO_BLK_PADDR, PAGE_R|PAGE_W); proc->pid = 100; proc->state = PROC_RUNNABLE; proc->sp = (uint32_t)sp; proc->page_table = pgtable; return proc; } void handle_syscall(struct trap_frame *f) { switch (f->a3) { case SYS_GETCHAR: while (1) { long ch = getchar(); if (ch >= 0) { f->a0 = ch; break; } yield(); } break; case SYS_EXIT: printf("process %d exited\n", current_proc->pid); current_proc->state = PROC_EXITED; yield(); while (1) {} case SYS_PUTCHAR: putchar(f->a0); break; case SYS_READFILE: // case SYS_WRITEFILE: { // const char *filename = (const char *)f->a0; // char *buf = (char *)f->a1; // int len = f->a2; // struct file *file = fs_lookup(filename); // if (!file) { // printf("file not found: %s\n", filename); // f->a0 = -1; // break; // } // if (f->a3 == SYS_READFILE) { // if (len > (int)file->size) { // len = file->size; // } // memcpy(buf, file->data, len); // f->a0 = len; // } else { // if (len > (int)sizeof(file->data)) { // len = (int)sizeof(file->data); // } // memcpy(file->data, buf, len); // file->size = len; // fs_flush(); // f->a0 = len; // } // } break; case SYS_THREAD_CREATE: { uint32_t ret = (uint32_t)kernel_create_thread((uint32_t)f->a0,(uint32_t)f->a1); f->a0 = ret; } break; case SYS_THREAD_EXIT: current_proc->state = PROC_EXITED; yield(); while (1) {} case SYS_THREAD_YIELD: yield(); break; default: PANIC("unexpected syscall a3=%x\n", f->a3); } } long getchar(void) { struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2); return ret.error; } // Virtio-blk definitions struct virtio_virtq *blk_request_vq; struct virtio_blk_req *blk_req; paddr_t blk_req_paddr; unsigned blk_capacity; uint32_t virtio_reg_read32(unsigned offset) { return *((volatile uint32_t*)(VIRTIO_BLK_PADDR + offset)); } // read 64 bits by two 32-bit reads uint64_t virtio_reg_read64(unsigned offset) { uint32_t low = virtio_reg_read32(offset); uint32_t high = virtio_reg_read32(offset + 4); return ((uint64_t)high << 32) | (uint64_t)low; } void virtio_reg_write32(unsigned offset, uint32_t value) { *((volatile uint32_t*)(VIRTIO_BLK_PADDR + offset)) = value; } void virtio_reg_fetch_and_or32(unsigned offset, uint32_t value) { uint32_t old = virtio_reg_read32(offset); virtio_reg_write32(offset, old | value); } struct virtio_virtq* virtq_init(unsigned index); // Initialize virtio-blk void virtio_blk_init(void) { if (virtio_reg_read32(VIRTIO_REG_MAGIC) != 0x74726976) PANIC("virtio: invalid magic"); if (virtio_reg_read32(VIRTIO_REG_VERSION) != 1) PANIC("virtio: invalid version"); if (virtio_reg_read32(VIRTIO_REG_DEVICE_ID) != VIRTIO_DEVICE_BLK) PANIC("virtio: invalid device id"); virtio_reg_write32(VIRTIO_REG_DEVICE_STATUS, 0); virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_ACK); virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_DRIVER); virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_FEAT_OK); blk_request_vq = virtq_init(0); virtio_reg_write32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_DRIVER_OK); blk_capacity = virtio_reg_read64(VIRTIO_REG_DEVICE_CONFIG) * SECTOR_SIZE; printf("virtio-blk: capacity is %d bytes\n", blk_capacity); blk_req_paddr = alloc_pages(1); blk_req = (struct virtio_blk_req*)blk_req_paddr; } // The virtqueue structure struct virtio_virtq { struct virtq_desc descs[VIRTQ_ENTRY_NUM]; struct virtq_avail avail; struct virtq_used used __attribute__((aligned(PAGE_SIZE))); int queue_index; volatile uint16_t *used_index; uint16_t last_used_index; }; struct virtio_virtq* virtq_init(unsigned index) { paddr_t vq_paddr = alloc_pages(1); struct virtio_virtq *vq = (struct virtio_virtq*)vq_paddr; vq->queue_index = index; vq->used_index = &vq->used.index; virtio_reg_write32(VIRTIO_REG_QUEUE_SEL, index); virtio_reg_write32(VIRTIO_REG_QUEUE_NUM, VIRTQ_ENTRY_NUM); virtio_reg_write32(VIRTIO_REG_QUEUE_ALIGN, 0); virtio_reg_write32(VIRTIO_REG_QUEUE_PFN, vq_paddr); return vq; } void virtq_kick(struct virtio_virtq *vq, int desc_index) { vq->avail.ring[vq->avail.index % VIRTQ_ENTRY_NUM] = desc_index; vq->avail.index++; __sync_synchronize(); virtio_reg_write32(VIRTIO_REG_QUEUE_NOTIFY, vq->queue_index); vq->last_used_index++; } bool virtq_is_busy(struct virtio_virtq *vq) { return vq->last_used_index != *vq->used_index; } // read/write disk // void read_write_disk(void *buf, unsigned sector, int is_write) { // if (sector >= blk_capacity / SECTOR_SIZE) { // printf("virtio: tried to read/write sector=%d, but capacity=%d\n", // sector, blk_capacity / SECTOR_SIZE); // return; // } // blk_req->sector = sector; // blk_req->type = is_write ? VIRTIO_BLK_T_OUT : VIRTIO_BLK_T_IN; // if (is_write) { // memcpy(blk_req->data, buf, SECTOR_SIZE); // } // struct virtio_virtq *vq = blk_request_vq; // vq->descs[0].addr = blk_req_paddr; // vq->descs[0].len = 4 * 2 + 8; // vq->descs[0].flags = VIRTQ_DESC_F_NEXT; // vq->descs[0].next = 1; // vq->descs[1].addr = blk_req_paddr + offsetof(struct virtio_blk_req, data); // vq->descs[1].len = SECTOR_SIZE; // vq->descs[1].flags = VIRTQ_DESC_F_NEXT | (is_write ? 0 : VIRTQ_DESC_F_WRITE); // vq->descs[1].next = 2; // vq->descs[2].addr = blk_req_paddr + offsetof(struct virtio_blk_req, status); // vq->descs[2].len = 1; // vq->descs[2].flags = VIRTQ_DESC_F_WRITE; // virtq_kick(vq, 0); // while (virtq_is_busy(vq)) {} // if (blk_req->status != 0) { // printf("virtio: warn: read/write sector=%d status=%d\n", sector, blk_req->status); // return; // } // if (!is_write) { // memcpy(buf, blk_req->data, SECTOR_SIZE); // } // } // File system structures struct file files[FILES_MAX]; uint8_t disk[DISK_MAX_SIZE]; // Utility to parse oct string int oct2int(char *oct, int len) { int dec = 0; for (int i = 0; i < len; i++) { if (oct[i] < '0' || oct[i] > '7') break; dec = dec * 8 + (oct[i] - '0'); } return dec; } // void fs_init(void) { // for (unsigned sector = 0; sector < sizeof(disk) / SECTOR_SIZE; sector++) { // read_write_disk(&disk[sector * SECTOR_SIZE], sector, 0); // } // unsigned off = 0; // for (int i = 0; i < FILES_MAX; i++) { // struct tar_header *hdr = (struct tar_header *)&disk[off]; // if (hdr->name[0] == '\0') break; // if (strcmp(hdr->magic, "ustar") != 0) { // PANIC("invalid tar header"); // } // int sz = oct2int(hdr->size, sizeof(hdr->size)); // char *np = hdr->name; // if (np[0] == '.' && np[1] == '/') { // np += 2; // } // struct file *f = &files[i]; // f->in_use = true; // strcpy(f->name, np); // memcpy(f->data, hdr->data, sz); // f->size = sz; // printf("file: %s, size=%d\n", f->name, f->size); // off += align_up(sizeof(struct tar_header) + sz, SECTOR_SIZE); // if (off >= sizeof(disk)) break; // } // } // void fs_flush(void) { // memset(disk, 0, sizeof(disk)); // unsigned off = 0; // for (int i = 0; i < FILES_MAX; i++) { // struct file *f = &files[i]; // if (!f->in_use) continue; // struct tar_header *hdr = (struct tar_header *)&disk[off]; // memset(hdr, 0, sizeof(*hdr)); // strcpy(hdr->name, f->name); // strcpy(hdr->mode, "000644"); // strcpy(hdr->magic, "ustar"); // strcpy(hdr->version, "00"); // hdr->type = '0'; // int sz = f->size; // for (int j = sizeof(hdr->size); j > 0; j--) { // hdr->size[j - 1] = (sz % 8) + '0'; // sz /= 8; // } // int csum = ' ' * sizeof(hdr->checksum); // for (unsigned k = 0; k < sizeof(struct tar_header); k++) { // csum += (unsigned char)disk[off + k]; // } // for (int j = 5; j >= 0; j--) { // hdr->checksum[j] = (csum % 8) + '0'; // csum /= 8; // } // memcpy(hdr->data, f->data, f->size); // off += align_up(sizeof(struct tar_header) + f->size, SECTOR_SIZE); // if (off >= sizeof(disk)) break; // } // for (unsigned sec = 0; sec < sizeof(disk) / SECTOR_SIZE; sec++) { // read_write_disk(&disk[sec * SECTOR_SIZE], sec, 1); // } // printf("wrote %d bytes to disk\n", (int)sizeof(disk)); // } struct file *fs_lookup(const char *filename) { for (int i = 0; i < FILES_MAX; i++) { if (!strcmp(files[i].name, filename)) { return &files[i]; } } return NULL; } // these come from shell.bin.o extern char _binary_shell_bin_start[]; extern char _binary_shell_bin_size[]; void kernel_main(void) { // Clear BSS memset(__bss, 0, (size_t)__bss_end - (size_t)__bss); printf("\n\n"); WRITE_CSR(stvec, (uint32_t)kernel_entry); // virtio_blk_init(); // Just test read sector 0 char tmp[SECTOR_SIZE]; // read_write_disk(tmp, 0, 0); printf("first sector: %s\n", tmp); // fs_init(); // Create idle process struct process* idle = create_process(NULL, 0); idle->pid = -1; current_proc = idle_proc = idle; // Debug: check if shell.bin actually got linked printf("DEBUG: _binary_shell_bin_start=%x, _binary_shell_bin_size=%x\n", _binary_shell_bin_start, (unsigned)_binary_shell_bin_size); size_t shell_size = (size_t)_binary_shell_bin_size; if (shell_size > 0) { create_process(_binary_shell_bin_start, shell_size); printf("Created shell process!\n"); } else { printf("WARNING: shell.bin is empty.\n"); } // If there is a valid shell image, create it as a process: yield(); // PANIC("switched to idle process"); while (1) { // The idle loop. If all processes exit, we might just hang here. } }

After commenting out all the disk-related parts, my code was indeed able to reach the kernel_main function and successfully print Created shell process! However, the error messages below still persisted. At this point, I decided to check if modifying other files could resolve the issue.
image

shell.c

#include "common.h" #include "user.h" void start(void){ while(1){ prompt: printf("> "); char cmd[128]; int i=0; for(;;){ int ch = getchar(); if(ch>=0) putchar((char)ch); if(i>= (int)(sizeof(cmd)-1)){ printf("\ncommand too long\n"); goto prompt; } if(ch=='\r' || ch=='\n'){ putchar('\n'); cmd[i]='\0'; break; } else { cmd[i++]=(char)ch; } } if(!strcmp(cmd,"hello")){ printf("Hello from shell!\n"); } else if(!strcmp(cmd,"exit")){ exit(); } else if(!strcmp(cmd,"testthread")){ extern void test_thread(void); test_thread(); } else { printf("unknown: %s\n", cmd); } } }

Next, I found a clue in the shell file. This is the shell file I updeted, but I discovered that when I removed the printf and putcharetc functions from the shell, the error messages disappeared. However, if I kept those functions, the error messages persisted, even when only the thread-related parts remained.

shell.c testing1

#include "common.h" #include "user.h" void start(void){ while(1){ }

image

shell.c testing2

#include "common.h" #include "user.h" void start(void){ while(1){ printf("> "); }

image

shell.c testing 3

#include "common.h" #include "user.h" void start(void){ extern void test_thread(void); test_thread(); }

image

shell.c testing4

#include "common.h" #include "user.h" void start(void){ while(1){ int x = 5; }

image
Finally, I tested the program by randomly adding a variable declaration in shell.c to see if it would trigger an error. Interestingly, no error occurred. This led me to believe that the issue is likely related to the custom-defined functions.

Separating the common files for the kernel and user

Because I suspected the issue might be related to the functions, I started wondering if the problem was caused by the common file being shared between the kernel and user sides. This could potentially lead to some shared functions inadvertently using kernel addresses, resulting in errors. To address this, I split common into common.c for the kernel and ucommon.c for the user and change all the file of user #include "common.c" to #include "ucommom.c". However, the results indicated that this wasn't the root of the problem.

ucommon.c

// ucommon.c #include <stdarg.h> // for va_list, va_start, va_end // #include "user.h" // for putchar, etc. #include "ucommon.h" void *memcpy(void *dst, const void *src, size_t n) { unsigned char *d = (unsigned char*)dst; const unsigned char *s = (const unsigned char*)src; while (n--) { *d++ = *s++; } return dst; } void *memset(void *dst, int c, size_t n) { unsigned char *p = (unsigned char*)dst; while (n--) { *p++ = (unsigned char)c; } return dst; } int strcmp(const char *s1, const char *s2) { while (*s1 && *s2) { if (*s1 != *s2) break; s1++; s2++; } return (unsigned char)*s1 - (unsigned char)*s2; } char *strcpy(char *dst, const char *src) { char *out = dst; while ((*dst++ = *src++)) /* empty */; return out; } // A simple user-level printf that calls user putchar void printf1(const char *fmt, ...) { va_list args; va_start(args, fmt); while (*fmt) { if (*fmt == '%') { fmt++; switch (*fmt) { case 's': { const char *s = va_arg(args, const char *); while (*s) { putchar(*s++); } break; } case 'd': { int val = va_arg(args, int); if (val < 0) { putchar('-'); val = -val; } // simple decimal int div = 1; while (val / div >= 10) { div *= 10; } while (div > 0) { putchar('0' + (val / div)); val %= div; div /= 10; } break; } case 'x': { unsigned v = va_arg(args, unsigned); for (int i = 0; i < 8; i++) { int shift = 28 - (i * 4); unsigned nib = (v >> shift) & 0xf; putchar("0123456789abcdef"[nib]); } break; } case '%': putchar('%'); break; default: putchar('%'); putchar(*fmt); break; } } else { putchar(*fmt); } fmt++; } va_end(args); }

ucommon.h

// ucommon.h #pragma once // Basic standard headers for user code #include <stdint.h> // for uint32_t, etc. #include <stddef.h> // for size_t #include <stdarg.h> // for va_list, va_start, etc. #ifndef NULL #define NULL ((void*)0) #endif // Syscall numbers needed by user code #define SYS_PUTCHAR 1 #define SYS_GETCHAR 2 #define SYS_EXIT 3 #define SYS_READFILE 4 #define SYS_WRITEFILE 5 #define SYS_THREAD_CREATE 6 #define SYS_THREAD_EXIT 7 #define SYS_THREAD_YIELD 8 // Function prototypes that user code expects void printf1(const char *fmt, ...); void putchar(char c); // Memory/string routines for user code void *memcpy(void *dst, const void *src, size_t n); void *memset(void *dst, int c, size_t n); int strcmp(const char *s1, const char *s2); char *strcpy(char *dst, const char *src);

image

run.sh

#!/bin/bash set -xue QEMU=qemu-system-riscv32 CC=clang CFLAGS="-std=c11 -O2 -g3 -Wall -Wextra --target=riscv32 -ffreestanding -nostdlib" OBJCOPY=/usr/bin/llvm-objcopy # 1) Build user program with ucommon.c $CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf \ shell.c test-thread.c pthread.c user.c ucommon.c # 2) Convert shell.elf -> shell.bin -> shell.bin.o $OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin $OBJCOPY -I binary -O elf32-littleriscv --set-section-flags .data=alloc,contents \ --rename-section .data=.shell_bin shell.bin shell.bin.o # 3) Build kernel, only compile kernel.c and kernel's common.c (not ucommon.c). $CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf \ kernel.c common.c shell.bin.o # 4) If there's a disk folder if [ -d disk ]; then (cd disk && tar cf ../disk.tar --format=ustar ./*.txt) fi # 5) Launch QEMU $QEMU -machine virt -bios default -nographic -serial mon:stdio --no-reboot \ -d unimp,guest_errors,int,cpu_reset -D qemu.log \ -drive id=drive0,file=disk.tar,format=raw,if=none \ -device virtio-blk-device,drive=drive0,bus=virtio-mmio-bus.0 \ -kernel kernel.elf

Non-existent address

Even after separating the files, the issue still persisted. I began to suspect that the problem might be caused by the address not existing at all—in other words, the user side might be attempting to access an address that doesn't exist, resulting in an error. To investigate further, I used some commands to trace the address, and it indeed seemed that no corresponding address could be found. Therefore, my current assumption is that the error occurs because the program is pointing to a non-existent address.
image
Although I only listed a portion of the instructions, I did thoroughly review all of them, and this address does not exist.
image

image

Conclusion

I have tried many methods, but I still couldn’t resolve the issue. Due to the limited time before the submission deadline, I was unable to implement the thread functionality in the mini operating system, which is truly regrettable. I hope to continue learning and, in the future, acquire the skills to better prevent and solve such problems.

References

os in 1000 lines
並行程式設計: 建立相容於 POSIX Thread 的實作