Phân tích cơ chế thực thi lệnh trong YEMU và NEMU

1. Quy trình thực thi lệnh trong YEMU

Quy trình thực thi lệnh trong YEMU được chia thành 3 bước cơ bản: Lấy lệnh (Fetch) → Giải mã (Decode) → Thực thi (Execute).

  • Lấy lệnh: YEMU sử dụng một mảng lệnh được định nghĩa sẵn (M[NMEM]). Mỗi lần lấy lệnh, con trỏ lệnh sẽ tuần tự trỏ tới phần tử tiếp theo trong mảng này.

uint8_t M[NMEM] = {
  0b11100110,  // load  6#     | R[0] <- M[y]
  0b00000100,  // mov   r1, r0 | R[1] <- R[0]
  0b11100101,  // load  5#     | R[0] <- M[x]
  0b00010001,  // add   r0, r1 | R[0] <- R[0] + R[1]
  0b11110111,  // store 7#     | M[z] <- R[0]
  0b00010000,  // x = 16
  0b00100001,  // y = 33
  0b00000000,  // z = 0
};
  • Giải mã: Dựa vào mã lệnh đã lấy được, YEMU thực hiện giải mã. Vì YEMU chỉ hỗ trợ 2 loại lệnh (R-type và M-type) nên thao tác giải mã được thực hiện đơn giản qua cấu trúc switch-case. Các cấu trúc DECODE_RDECODE_M được định nghĩa sẵn để lấy ra các toán hạng như rd, src1, src2.

#define DECODE_R(inst) uint8_t rt = (inst).rtype.rt, rs = (inst).rtype.rs
#define DECODE_M(inst) uint8_t addr = (inst).mtype.addr
  • Thực thi: Sau khi giải mã, YEMU thực hiện lệnh tương ứng trong mỗi nhánh case, sau đó tăng pc lên 1 đơn vị để chuẩn bị cho lần lấy lệnh tiếp theo. Quá trình này lặp lại cho đến khi gặp lệnh kết thúc.

    case 0b0000: { DECODE_R(this); R[rt]   = R[rs];   break; }
    case 0b0001: { DECODE_R(this); R[rt]  += R[rs];   break; }
    case 0b1110: { DECODE_M(this); R[0]    = M[addr]; break; }
    case 0b1111: { DECODE_M(this); M[addr] = R[0];    break; }
    default: // halt

2. Quy trình thực thi lệnh trong NEMU

Nhìn chung, NEMU cũng tuân theo quy trình 3 bước: Lấy lệnh - Giải mã - Thực thi. Tuy nhiên, mỗi bước đều phức tạp hơn YEMU rất nhiều.

  • Lấy lệnh: NEMU kiểm tra sự tồn tại của file nhị phân (.bin). Nếu không có, nó sử dụng mảng lệnh định nghĩa sẵn và đọc lệnh thông qua hàm pmem_read(). Bản chất đây là thao tác đọc từ bộ nhớ.
  • Giải mã: Quá trình này phức tạp hơn YEMU do kiến trúc RISC-V 32-bit hỗ trợ nhiều kiểu lệnh: I, U, S, J, R, B. Mỗi kiểu lệnh có cấu trúc giải mã riêng, được định nghĩa bằng các macro.

#define src1R() do { *src1 = R(rs1); } while (0)
#define src2R() do { *src2 = R(rs2); } while (0)
#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)
#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)
#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)
#define immJ() do { *imm = SEXT((BITS(i,31,31)<<19 |BITS(i,30,21)|BITS(i,20,20)<<10|\
                                 (BITS(i,19,12)<<11))<<1, 21); } while(0)
#define immB() do {*imm = SEXT((BITS(i,31,31)<<11|BITS(i,30,25)<<4|BITS(i,11,8)|BITS(i,8,7)<<10)<<1,13);} while(0)

    case TYPE_I: src1R();          immI(); break;
    case TYPE_U:                   immU(); break;
    case TYPE_S: src1R(); src2R(); immS(); break;
    case TYPE_J:                   immJ(); break;
    case TYPE_R: src1R();  src2R();        break;
    case TYPE_B: src1R(); src2R(); immB(); break;
  • Thực thi: NEMU sử dụng các macro INSTPAT để ánh xạ mã lệnh tới hành vi thực thi. Về bản chất, đây cũng là một cấu trúc switch-case nhưng linh hoạt hơn.

  INSTPAT("??????? ????? ????? 000 ????? 00100 11", addi   , I, R(rd) = src1 + imm);  
  INSTPAT("??????? ????? ????? ??? ????? 01101 11", lui    , U, R(rd) = imm );
  INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc  , U, R(rd) = s->pc + imm);

Sau khi thực thi, pc được cập nhật thành next_pc. Nếu chưa kết thúc, quá trình lặp lại từ bước lấy lệnh.

3. Cách thức hoạt động của trò chơi gõ chữ mini

Trò chơi hoạt động dựa trên vòng lặp chính: Làm mới màn hình → Nhận đầu vào → Cập nhật trạng thái trò chơi.

  • Khởi tạo: Hàm main() khởi tạo giao diện IOE (Input/Output Engine) cho 26 phím chữ cái, sau đó khởi tạo màn hình VGA với màu nền tím đồng nhất.
  • Vòng lặp chính: Trong vòng lặp while(1), trò chơi thực hiện 3 bước:
    1. Hiển thị chữ: Các chữ cái mới xuất hiện trên màn hình VGA, đồng thời vị trí của các chữ đang rơi được cập nhật.
    2. Xử lý chữ chạm đáy: Các chữ rơi chạm đáy màn hình sẽ bị loại bỏ và tính là "miss".
    3. Nhận đầu vào: Sử dụng giao diện KEYBRD có sẵn để kiểm tra xem phím vừa nhấn có trùng với chữ cái đang rơi không (check_hit()).
  • Cập nhật VGA: Cuối mỗi vòng lặp, trò chơi đọc buffer hiện tại, ghi trạng thái mới vào buffer để chuẩn bị cho lần hiển thị tiếp theo.

4. Phân tích lỗi khi bỏ staticinline khỏi hàm inst_fetch()

Hàm inst_fetch() được định nghĩa với cả hai từ khóa staticinline. Khi thử nghiệm bỏ một trong hai hoặc cả hai, kết quả biên dịch như sau:

  • Bỏ static: Không có lỗi. Lý do là trình biên dịch có thể tạo ra các ký hiệu yếu (weak symbols). Với inline function, nếu có nhiều định nghĩa, chỉ một trong số chúng được giữ lại khi liên kết.
  • Bỏ inline: Không có lỗi. Vì static vẫn còn, hàm có internal linkage (chỉ hiển thị trong file nguồn hiện tại). Do đó, không xảy ra xung đột khi liên kết nhiều file object.
  • Bỏ cả staticinline: Gây ra lỗi liên kết (linker error): multiple definition of 'inst_fetch'. Lỗi này xuất hiện vì hàm mất đi internal linkage và cũng không được tạo thành weak symbol. Kết quả là nhiều file object cùng định nghĩa một hàm có tên giống nhau, dẫn đến xung đột.

Thẻ: YEMU NEMU RISC-V biên dịch liên kết

Đăng vào ngày 1 tháng 6 lúc 21:28