iT邦幫忙

2021 iThome 鐵人賽

DAY 2
4
Software Development

猴子都寫得出來的 RISC-V CPU Emulator系列 第 30

Hello: 真的哈囉

今天我想來點... Hello World!
又是快樂 Debug 的好日子呢!

昨天已經把程式跑起來了,
但又發現了很多奇怪的事情。
HEX Format 每個欄位到底代表什麼?
為什麼 Program Loader 跟 ELF 格式的開始位置對不起來?
真的要 * 16 嗎?
今天就來嘗試解開這些謎團吧!

可惜 HEX Format 的資料太零散,找到的原檔連結也都失效,
只有第二手資訊真的不好整理,
找了很久 0203 格式的用法還不夠確定,
歡迎熟悉的朋友幫忙補充。

HEX File Format

因為 Intel 官方和 Wikipedia 上面的運算都沒寫清楚,
我們參考 arm 的文件 HEX File Format
很明顯地跟程式寫的一樣,每一行都分成 00、02、03、04、05等 Type。
檔案中包含了程式開始位置、資料位置以及內容等資訊。

HEX File 單行格式:

  • 每一行::llaaaatt[dd...]cc
    • : 每一行 HEX 格式的第一個字元固定都是這個
    • ll 這一行的 [dd...] 有多少 byte
    • aaaa 代表接下來的資料從哪個 address 開始
    • tt 這一行後面的內容代表什麼
    • [dd...] 這一行所帶的資料
    • cc 用來驗證前面內容是否無誤的 checksum

HEX File 單行資料的意義是由 tt 欄位決定:

  • 00 Data Record
    範例::10 0054 00 130101FD2326810213040103232EA4FC B2
    10 代表有 16 byte 的資料
    0054 資料位置從 0x0054 開始
    00 這行的資料為 Data Record
    130101FD2326810213040103232EA4FC 16 byte 資料
  • 01 End-of-File (EOF) Record
    範例::00000001FF
    內容固定,想知道細節可以參考上面提到的文件
  • 02 Extended Segment Address Record
    範例::02 0000 02 1000 EC
    02 代表有 2 byte 的資料
    0000 這邊永遠為 0
    02 這行的資料為 Extended Segment Address
    1000 8086 segment 是 16-byte alignment
  • 03 Start Segment Address Record
    範例::04 0000 03 100000B8 31
    04 代表有 4 byte 的資料
    0000 這邊永遠為 0
    03 這行的資料為 Start Segment Address Record
    1000 8086 Code segment (CS)暫存器數值,是 16-byte alignment
    00B8 8086 Instruction pointer(IP) 暫存器數值
  • 04 Extended Address Record
    範例::02 0000 04 00FF FB
    02 代表有 2 byte 的資料
    0000 這邊永遠為 0
    02 這行的資料為 Extended Address Record
    00FF 代表最高的兩個 byte offset,在這邊是 0x00FF0000
  • 05 Start Linear Address Record
    範例::04 0000 05 08000121 CD
    04 代表有 4 byte 的資料
    0000 這邊永遠為 0
    05 這行的資料為 Start Linear Address Record
    08000121 代表載入 8086 的 EIP 暫存器位置,也就是 32 bit Program Counter

以下是這次的 hello.c 編譯出來的 binary.hex
可以看到程式起始位置為 0x1000 * 16 + 0x00B8 = 0x1000B8
Extended Address 為 0x1000 * 16 = 0x100000

//binary.hex
      type
       ||
:020000021000EC                              //extend segment address
:10005400130101FD2326810213040103232EA4FCB2  //data
:10006400232604FE6F0080028327C4FE0327C4FDF9  //data
:100074003307F700B7074000034707002380E70072  //data
:100084008327C4FE938717002326F4FE8327C4FE28  //data
:100094000327C4FDB307F70083C70700E39607FCF3  //data
:1000A40013000000138507000324C1021301010398  //data
:1000B40067800000130101FF23261100232481001F  //data
:1000C40013040101B70701001385C70EEFF05FF8B1  //data
:1000D40093070000138507008320C10003248100D7  //data
:0800E400130101016780000017                  //data
:0D00EC0068656C6C6F20776F726C6421008A        //data
:04000003100000B831                          //start segment address
:00000001FF                                  //end of hex file

這下子終於看懂 /* ? */ 是什麼意思了!
大概是因為原作者沒用到所以不知道這行在幹嘛XD

//RISC-V-TLM by mariusmm
uint32_t code_segment;
code_segment = stol(line.substr(9, 4), nullptr, 16) * 16; /* ? */

研究一下別人的 8086 assembly code 發現 Extended Segment Address 最終會放到 bp 內,
看起來是 Stack Frame 分隔的 Base Pointer,
有興趣的人可以在該檔案搜尋 "Intel-hex file" ,就可以看到了。

也就是說這兩個分別是起始的 Program Counter 和 Stack Pointer 位置,
但這樣一來,Program Memory Layout 又說不通了,
Stack Section 竟然是在比 Text Section 低的位置?

恩....看起來還要再花時間研究一下,
不過夜深了,猴子也累了,今天就先寫到這裡吧!

大家最熟悉的Debug開發流程

hello world 寫起來很簡單,
但是執行的時候卻碰到問題:
沒有看到預期的的字元被一個一個寫進指定的 address。

第一個步驟就是把 hello.o 反組譯之後一條一條指令核對!
Function Call 的 JALJALR 看起來沒什麼問題,
就來看 printToTrace 的內容:

00010054 <printToTrace>:
printToTrace():
   10054:       fd010113                addi    sp,sp,-48
   10058:       02812623                sw      s0,44(sp)
   1005c:       03010413                addi    s0,sp,48
   10060:       fca42e23                sw      a0,-36(s0)
   10064:       fe042623                sw      zero,-20(s0)
   10068:       0280006f                j       10090 <printToTrace+0x3c>
   1006c:       fec42783                lw      a5,-20(s0)
   10070:       fdc42703                lw      a4,-36(s0)
   10074:       00f70733                add     a4,a4,a5
   10078:       004007b7                lui     a5,0x400
   1007c:       00074703                lbu     a4,0(a4)
   10080:       00e78023                sb      a4,0(a5) # 400000 <__global_pointer$+0x3ee707>
   10084:       fec42783                lw      a5,-20(s0)
   10088:       00178793                addi    a5,a5,1
   1008c:       fef42623                sw      a5,-20(s0)
   10090:       fec42783                lw      a5,-20(s0)
   10094:       fdc42703                lw      a4,-36(s0)
   10098:       00f707b3                add     a5,a4,a5
   1009c:       0007c783                lbu     a5,0(a5)
   100a0:       fc0796e3                bnez    a5,1006c <printToTrace+0x18>
   100a4:       00000013                nop
   100a8:       00078513                mv      a0,a5
   100ac:       02c12403                lw      s0,44(sp)
   100b0:       03010113                addi    sp,sp,48
   100b4:       00008067                ret

發現又是 branch 的時候出問題,
它竟然直接跳到下一行 0x10d4 結束 loop,
而不是跳到 0x1006c 執行 loop 內容!

 rdValue: 0x3fff0 immValue: 0x30
current_pc: 0x10b4 target_pc: 0x10d4 JALR 1 0 0 rs1Value: 0x10d4 rs2Value: 0x0 r

但是東看西看,前天改 BNE 的沒問題!
往回追一道指令,發現 LBU 拿到 0,
也就是說 "hello world!" 的位置從第一個 byte 就是 0,
那個 address... 是不是怪怪的啊!
0x100ec 看起來就跟 HEX File算出來的 main 起始位置 0x10b8 差很遠,
肯定哪裡怪怪的!
P.S.ADD 數值也怪怪的,不過是 Logger 的問題,先不理它!

current_pc: 0x1068 target_pc: 0x1090 JAL 0 8 0 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x28
current_pc: 0x1090 target_pc: 0x1094 LW 8 12 15 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x0 immValue: 0xffffffec
current_pc: 0x1094 target_pc: 0x1098 LW 8 28 14 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x100ec immValue: 0xffffffdc
current_pc: 0x1098 target_pc: 0x109c ADD 14 15 15 rs1Value: 0x100ec rs2Value: 0x100ec rdValue: 0x100ec immValue: 0x0
current_pc: 0x109c target_pc: 0x10a0 LBU 15 0 15 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x0
current_pc: 0x10a0 target_pc: 0x10a4 BNE 15 0 13 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0xffffffcc

假設是 Data Section 拿錯位置,
0x10b8 要對起來應該是 0x100b8
看起來是該改回 * 16 的時候了!

//memory.cpp
...
                        case 2: { //Extended segment address
                                extended_address = std::stoul(line.substr(9, 4), nullptr, 16) * 16;
                        }
                        break;
                        case 3: { //Start segment address
                                uint32_t code_segment = stoul(line.substr(9, 4), nullptr, 16) * 16;
...

Bingo!!!
果然是它!
前幾天沒有找到正確資料的報應立刻就來了。
之後好好地把 HEX Format Program Loader 修好吧!

實際程式

github 頁面 Tag: ITDay30

先在 Bus 裡面加了一個簡單的 console,
使用 Memory Mapped I/O 做法,
只要存取指定的 address space 就是存取指定的 I/O device。

本來打算另外寫一篇完整一點的 External Device 章節,
但時間不太夠,只能之後再補了。

//bus.cpp
...
        switch (addr) {
                case CONSOLE_BASE:
                        std::cout << "console: \'" << *reinterpret_cast<unsigned char*>(&data) << "\'" << std::endl;
                        break;
                default:
                        memory_socket->b_transport(trans, delay);
                        break;
        }
...
//bus.h
...
        enum memoryMapp {
                MEMORY_BASE = 0x000000,
                CONSOLE_BASE = 0x400000,
        };
...

程式非常簡單,
就是把 "hello world!" 依序寫入上面設定的 address。

//test.c
#define CONSOLE (*(unsigned char *)0x400000)

int printToTrace(char* input)
{
        int i=0;
        while(input[i] != '\0') {
                CONSOLE = input[i];
                i++;
        }
}

int main()
{
        printToTrace("hello world!");
        return 0;
}

執行結果

current_pc: 0x100b8 target_pc: 0x100bc ADDI 2 16 2 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x3fff0 immValue: 0xfffffff0
current_pc: 0x100bc target_pc: 0x100c0 SW 2 1 12 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x0 immValue: 0xc
current_pc: 0x100c0 target_pc: 0x100c4 SW 2 8 8 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x8
current_pc: 0x100c4 target_pc: 0x100c8 ADDI 2 16 8 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x40000 immValue: 0x10
current_pc: 0x100c8 target_pc: 0x100cc LUI 2 0 15 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x10000 immValue: 0x10
current_pc: 0x100cc target_pc: 0x100d0 ADDI 15 12 10 rs1Value: 0x10000 rs2Value: 0x0 rdValue: 0x100ec immValue: 0xec
current_pc: 0x100d0 target_pc: 0x10054 JAL 31 5 1 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x100d4 immValue: 0xffffff84
current_pc: 0x10054 target_pc: 0x10058 ADDI 2 16 2 rs1Value: 0x3ffc0 rs2Value: 0x0 rdValue: 0x3ffc0 immValue: 0xffffffd0
current_pc: 0x10058 target_pc: 0x1005c SW 2 8 12 rs1Value: 0x3ffc0 rs2Value: 0x40000 rdValue: 0x0 immValue: 0x2c
current_pc: 0x1005c target_pc: 0x10060 ADDI 2 16 8 rs1Value: 0x3ffc0 rs2Value: 0x0 rdValue: 0x3fff0 immValue: 0x30
current_pc: 0x10060 target_pc: 0x10064 SW 8 10 28 rs1Value: 0x3fff0 rs2Value: 0x100ec rdValue: 0x0 immValue: 0xffffffdc
current_pc: 0x10064 target_pc: 0x10068 SW 8 0 12 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x0 immValue: 0xffffffec
current_pc: 0x10068 target_pc: 0x10090 JAL 0 8 0 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x28
current_pc: 0x10090 target_pc: 0x10094 LW 8 12 15 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x0 immValue: 0xffffffec
current_pc: 0x10094 target_pc: 0x10098 LW 8 28 14 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x100ec immValue: 0xffffffdc
current_pc: 0x10098 target_pc: 0x1009c ADD 14 15 15 rs1Value: 0x100ec rs2Value: 0x100ec rdValue: 0x100ec immValue: 0x0
current_pc: 0x1009c target_pc: 0x100a0 LBU 15 0 15 rs1Value: 0x68 rs2Value: 0x0 rdValue: 0x68 immValue: 0x0
current_pc: 0x100a0 target_pc: 0x1006c BNE 15 0 13 rs1Value: 0x68 rs2Value: 0x0 rdValue: 0x0 immValue: 0xffffffcc
current_pc: 0x1006c target_pc: 0x10070 LW 8 12 15 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x0 immValue: 0xffffffec
current_pc: 0x10070 target_pc: 0x10074 LW 8 28 14 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x100ec immValue: 0xffffffdc
current_pc: 0x10074 target_pc: 0x10078 ADD 14 15 14 rs1Value: 0x100ec rs2Value: 0x0 rdValue: 0x100ec immValue: 0x0
current_pc: 0x10078 target_pc: 0x1007c LUI 0 4 15 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x400000 immValue: 0x400
current_pc: 0x1007c target_pc: 0x10080 LBU 14 0 14 rs1Value: 0x68 rs2Value: 0x0 rdValue: 0x68 immValue: 0x0
console: h
...
console: e
...
console: l
...
console: l
...
console: o
...
console:  
...
console: w
...
console: o
...
console: r
...
console: l
...
console: d
...
console: !
current_pc: 0x10080 target_pc: 0x10084 SB 15 14 0 rs1Value: 0x400000 rs2Value: 0x21 rdValue: 0x0 immValue: 0x0
current_pc: 0x10084 target_pc: 0x10088 LW 8 12 15 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0xb immValue: 0xffffffec
current_pc: 0x10088 target_pc: 0x1008c ADDI 15 1 15 rs1Value: 0xc rs2Value: 0x0 rdValue: 0xc immValue: 0x1
current_pc: 0x1008c target_pc: 0x10090 SW 8 15 12 rs1Value: 0x3fff0 rs2Value: 0xc rdValue: 0x0 immValue: 0xffffffec
current_pc: 0x10090 target_pc: 0x10094 LW 8 12 15 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0xc immValue: 0xffffffec
current_pc: 0x10094 target_pc: 0x10098 LW 8 28 14 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x100ec immValue: 0xffffffdc
current_pc: 0x10098 target_pc: 0x1009c ADD 14 15 15 rs1Value: 0x100ec rs2Value: 0x100f8 rdValue: 0x100f8 immValue: 0x0
current_pc: 0x1009c target_pc: 0x100a0 LBU 15 0 15 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x0
current_pc: 0x100a0 target_pc: 0x100a4 BNE 15 0 13 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0xffffffcc
current_pc: 0x100a4 target_pc: 0x100a8 ADDI 0 0 0 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x0
current_pc: 0x100a8 target_pc: 0x100ac ADDI 15 0 10 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x0
current_pc: 0x100ac target_pc: 0x100b0 LW 2 12 8 rs1Value: 0x3ffc0 rs2Value: 0x0 rdValue: 0x40000 immValue: 0x2c
current_pc: 0x100b0 target_pc: 0x100b4 ADDI 2 16 2 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x3fff0 immValue: 0x30
current_pc: 0x100b4 target_pc: 0x100d4 JALR 1 0 0 rs1Value: 0x100d4 rs2Value: 0x0 rdValue: 0x0 immValue: 0x0
current_pc: 0x100d4 target_pc: 0x100d8 ADDI 0 0 15 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x0
current_pc: 0x100d8 target_pc: 0x100dc ADDI 15 0 10 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x0
current_pc: 0x100dc target_pc: 0x100e0 LW 2 12 1 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x0 immValue: 0xc
current_pc: 0x100e0 target_pc: 0x100e4 LW 2 8 8 rs1Value: 0x3fff0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x8
current_pc: 0x100e4 target_pc: 0x100e8 ADDI 2 16 2 rs1Value: 0x40000 rs2Value: 0x0 rdValue: 0x40000 immValue: 0x10
current_pc: 0x100e8 target_pc: 0x0 JALR 1 0 0 rs1Value: 0x0 rs2Value: 0x0 rdValue: 0x0 immValue: 0x0
INVALID: Opcode :0
Illegal Instruction, end simulation!
exception!

Info: /OSCI/SystemC: Simulation stopped by user.

結語

花了 30 天,終於寫出自己的 hello world 了。

最初的最初那篇,就是在哈囉。
只是踏著前人的足跡,用著前人留下的工具。

最後的最後這篇,還是在哈囉。
但這次不一樣,
我們有了自己建立的工具,
還多了看著這篇文章的你我 :-D


上一篇
Hello World: 編譯環境建立
下一篇
以終為始
系列文
猴子都寫得出來的 RISC-V CPU Emulator31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言