Thiết kế và Hiện thực hóa Trình mô phỏng Kiến trúc Máy tính Y86

Kiến trúc Y86 là một phiên bản rút gọn của x86-64, được thiết kế cho mục đích giáo dục nhằm giúp sinh viên và kỹ sư hiểu rõ cách thức hoạt động của bộ vi xử lý ở mức thấp nhất. Việc xây dựng một trình mô phỏng (simulator) và trình biên dịch (assembler) cho Y86 đòi hỏi sự hiểu biết sâu sắc về vòng đời của lệnh, quản lý bộ nhớ và trạng thái của CPU.

1. Tổng quan về tập lệnh Y86

Tập lệnh Y86 bao gồm 15 thanh ghi 64-bit (từ %rax đến %r14), bộ đếm chương trình (PC), và các mã điều kiện (Condition Codes) như ZF (Zero), SF (Sign), và OF (Overflow). Các lệnh trong Y86 có độ dài biến thiên từ 1 đến 10 byte, tùy thuộc vào việc chúng có chứa các trường thanh ghi hoặc giá trị hằng số (immediate) hay không.

Các nhóm lệnh chính bao gồm:

  • Di chuyển dữ liệu: irmovq (hằng số vào thanh ghi), rrmovq (thanh ghi sang thanh ghi), mrmovq (bộ nhớ vào thanh ghi), rmmovq (thanh ghi vào bộ nhớ).
  • Xử lý số học/logic: addq, subq, andq, xorq.
  • Điều khiển luồng: jmp, jXX (nhảy có điều kiện), call, ret.
  • Thao tác ngăn xếp: pushq, popq.

2. Thiết kế cấu trúc trình mô phỏng

Một trình mô phỏng hiệu quả cần mô hình hóa chính xác trạng thái phần cứng. Chúng ta có thể sử dụng cấu trúc dữ liệu trong C++ để đại diện cho bộ vi xử lý Y86.

struct Y86Processor {
    uint64_t registers[15];
    uint64_t pc;
    uint8_t* memory;
    size_t memory_size;
    
    struct {
        bool zf; // Zero Flag
        bool sf; // Sign Flag
        bool of; // Overflow Flag
    } codes;

    enum Status { AOK, HLT, ADR, INS };
    Status state;

    Y86Processor(size_t mem_size) {
        memory = new uint8_t[mem_size];
        memset(memory, 0, mem_size);
        memset(registers, 0, sizeof(registers));
        pc = 0;
        state = AOK;
    }
};

3. Vòng lặp thực thi lệnh

Quá trình thực thi của trình mô phỏng tuân theo mô hình tuần tự: Fetch (Lấy lệnh), Decode (Giải mã), Execute (Thực thi), Memory (Truy cập bộ nhớ), Write-back (Ghi kết quả) và PC Update (Cập nhật bộ đếm chương trình).

Giải mã lệnh (Fetch & Decode)

Trong giai đoạn này, trình mô phỏng đọc byte tại vị trí PC để xác định loại lệnh và các tham số đi kèm.

void step(Y86Processor &cpu) {
    uint8_t opcode = cpu.memory[cpu.pc];
    uint8_t high_nibble = opcode >> 4;
    uint8_t low_nibble = opcode & 0x0F;

    switch (high_nibble) {
        case 0x0: // halt
            cpu.state = Y86Processor::HLT;
            break;
        case 0x1: // nop
            cpu.pc += 1;
            break;
        case 0x3: { // irmovq V, rB
            uint8_t regs = cpu.memory[cpu.pc + 1];
            uint8_t rB = regs & 0x0F;
            uint64_t valV = *(uint64_t*)(&cpu.memory[cpu.pc + 2]);
            cpu.registers[rB] = valV;
            cpu.pc += 10;
            break;
        }
        // Các trường hợp khác...
    }
}

4. Xử lý logic số học và mã điều kiện

Các lệnh ALU không chỉ tính toán giá trị mà còn phải cập nhật các cờ trạng thái để phục vụ cho các lệnh nhảy có điều kiện sau đó. Ví dụ, lệnh subq %rax, %rbx thực hiện %rbx = %rbx - %rax.

void execute_sub(Y86Processor &cpu, uint8_t rA, uint8_t rB) {
    int64_t valA = (int64_t)cpu.registers[rA];
    int64_t valB = (int64_t)cpu.registers[rB];
    int64_t result = valB - valA;

    cpu.registers[rB] = result;

    // Cập nhật mã điều kiện
    cpu.codes.zf = (result == 0);
    cpu.codes.sf = (result < 0);
    cpu.codes.of = ((valA > 0 && valB < 0 && result > 0) || 
                    (valA < 0 && valB > 0 && result < 0));
}

5. Quản lý điều khiển luồng

Lệnh nhảy (jump) và gọi hàm (call) làm thay đổi PC một cách không tuần tự. Lệnh call yêu cầu lưu địa chỉ trả về vào ngăn xếp trước khi nhảy đến địa chỉ đích.

void handle_call(Y86Processor &cpu, uint64_t dest) {
    // Giảm stack pointer (%rsp nằm ở chỉ số 4)
    cpu.registers[4] -= 8;
    uint64_t rsp = cpu.registers[4];
    
    // Lưu địa chỉ trả về (địa chỉ sau lệnh call)
    uint64_t ret_addr = cpu.pc + 9;
    *(uint64_t*)(&cpu.memory[rsp]) = ret_addr;
    
    // Nhảy tới đích
    cpu.pc = dest;
}

6. Trình biên dịch Y86 (Assembler)

Nhiệm vụ của trình biên dịch là chuyển đổi mã nguồn dạng văn bản (.s) sang định dạng nhị phân (.bin hoặc .yo). Quá trình này thường bao gồm hai giai đoạn (two-pass):

  1. Pass 1: Xây dựng bảng ký hiệu (Symbol Table) bằng cách quét qua mã nguồn để xác định địa chỉ của các nhãn (labels).
  2. Pass 2: Chuyển đổi từng dòng lệnh thành mã máy, thay thế các nhãn bằng địa chỉ cụ thể đã tìm được ở Pass 1 và xử lý các hằng số.

Mô hình lưu trữ dữ liệu của Y86 tuân theo quy tắc Little-endian, trong đó byte thấp nhất của một số đa byte được lưu trữ tại địa chỉ thấp nhất. Điều này cực kỳ quan trọng khi xử lý các giá trị 64-bit trong trình biên dịch và trình mô phỏng.

7. Mô hình bộ nhớ

Bộ nhớ trong Y86 là một mảng byte tuyến tính. Trình mô phỏng cần xử lý các lỗi truy cập bộ nhớ như truy cập ngoài phạm vi hoặc truy cập không căn chỉnh (unaligned access) nếu cấu trúc phần cứng yêu cầu. Trong các bài tập lớn (Labs), việc phân chia bộ nhớ thành các phân đoạn (code, data, stack) giúp việc gỡ lỗi trở nên dễ dàng hơn.

Kết hợp trình mô phỏng và trình biên dịch, chúng ta tạo ra một hệ sinh thái hoàn chỉnh để thực thi các chương trình viết bằng hợp ngữ Y86, từ đó hiểu sâu hơn về kiến trúc hệ thống, cách thức hoạt động của pipeline (nếu mở rộng) và sự tương tác giữa phần mềm và phần cứng.

Thẻ: Y86 Computer Architecture C++ Simulator Assembler

Đăng vào ngày 20 tháng 5 lúc 09:36