iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 9
0
Software Development

系統架構秘辛:了解RISC-V 架構底層除錯器的秘密!系列 第 9

Day 09: RISC-V Debug Module (中篇)-Program Buffer & Abstract Commands

0. 前言

前偏簡單的介紹了Debug Module所提供的功能以及Target狀態控制...等等,
今天就要來探討一下Debug Module中的核心功能"Abstract Commands"和"Program Buffer"
  
  
  

1. Program Buffer

Program Buffer存在的主要目的就是可以在Target上,讓Debugger可以撰寫一些小程式,
執行任意的指令!

另外,根據Spec.中的定義,Program Buffer最後一道指令必須是"ebreak"或是"c.ebreak"之外,其餘的指令皆可任意在Program Buffer上執行,不分16-bits或是32-bits的指令皆可!

以下舉幾個簡單的例子和實作分別說明Program Buffer的用處!

  • Access Registers
  • Access Memory
      
      

1.1 Read Registers

先看一個簡單的Read GPR $x1的例子(64-bits的平台)

sd      $x1, 0x98(zero)
fence
ebreak

簡單的三行指令就可以把$x1搬到0x98的位置存放起來!
然後只要從那邊讀出資料,就可以知道$x1的內容!
實作方式,請參考以下內容(src/target/riscv/riscv-013.c的古早版本):

static int register_read_direct(struct target *target, uint64_t *value, uint32_t number)
{
        struct riscv_program program;
        riscv_program_init(&program, target);
        riscv_addr_t output = riscv_program_alloc_d(&program);
        riscv_program_write_ram(&program, output + 4, 0);
        riscv_program_write_ram(&program, output, 0);

        assert(GDB_REGNO_XPR0 == 0);
        if (number <= GDB_REGNO_XPR31) {
            riscv_program_sx(&program, number, output);     ///譯註: 裡面再去區分需要sd(64-bits) / sw(32-bits)
        } else if (number >= GDB_REGNO_FPR0 && number <= GDB_REGNO_FPR31) {
            riscv_program_fsx(&program, number, output);
        } else if (number >= GDB_REGNO_CSR0 && number <= GDB_REGNO_CSR4095) {
            LOG_DEBUG("reading CSR index=0x%03x", number - GDB_REGNO_CSR0);
            enum gdb_regno temp = riscv_program_gettemp(&program);
            riscv_program_csrr(&program, temp, number);
            riscv_program_sx(&program, temp, output);
        } else {
            LOG_ERROR("Unsupported register (enum gdb_regno)(%d)", number);
            abort();
        }

        int exec_out = riscv_program_exec(&program, target);    ///譯註:裡面會補上fence和ebreak,並執行Program Buffer!
        if (exec_out != ERROR_OK) {
            riscv013_clear_abstract_error(target);
            return ERROR_FAIL;
        }

        *value = 0;
        *value |= ((uint64_t)(riscv_program_read_ram(&program, output + 4))) << 32;
        *value |= riscv_program_read_ram(&program, output);

        LOG_DEBUG("[%d] reg[0x%x] = 0x%" PRIx64, riscv_current_hartid(target),
                number, *value);
        return ERROR_OK;
}

  
  

1.2 Write Registers

同上一節的Read Registers,我們考慮以下範例:

ld      x1, 0x98(zero)
fence
ebreak

簡單的三行指令就可以把0x98中的資料搬到$x1的位置存放起來!
實作方式,請參考以下內容(src/target/riscv/riscv-013.c的古早版本):

static int register_write_direct(struct target *target, unsigned number,
        uint64_t value)
{
    LOG_DEBUG("[%d] reg[0x%x] <- 0x%" PRIx64, riscv_current_hartid(target),
            number, value);

    struct riscv_program program;
    riscv_program_init(&program, target);
    riscv_addr_t input = riscv_program_alloc_d(&program);
    riscv_program_write_ram(&program, input + 4, value >> 32);
    riscv_program_write_ram(&program, input, value);

    assert(GDB_REGNO_XPR0 == 0);
    if (number <= GDB_REGNO_XPR31) {
        riscv_program_lx(&program, number, input);     ///譯註: 裡面再去區分需要ld(64-bits) / lw(32-bits)
    } else if (number >= GDB_REGNO_FPR0 && number <= GDB_REGNO_FPR31) {
        riscv_program_flx(&program, number, input);
    } else if (number >= GDB_REGNO_CSR0 && number <= GDB_REGNO_CSR4095) {
        enum gdb_regno temp = riscv_program_gettemp(&program);
        riscv_program_lx(&program, temp, input);
        riscv_program_csrw(&program, temp, number);
    } else {
        LOG_ERROR("Unsupported register (enum gdb_regno)(%d)", number);
        abort();
    }

    int exec_out = riscv_program_exec(&program, target);
    if (exec_out != ERROR_OK) {
        riscv013_clear_abstract_error(target);
        return ERROR_FAIL;
    }

    return ERROR_OK;
}

基本上流程同Read Registers
  
  

1.3 Read Memory

先看一個簡單的Read GPR $x1的例子(64-bits的平台)

addi    s0,s0,4
lw      s1,0(s0)
sw      s1,0x9c(zero)
fence   unknown,unknown
ebreak

假設$s0中負責存目標的Address,$s1中負責存放Data!
以上程式的流程如下:

  1. S0 = S0 + 4 //前進到下一個Word
  2. 將Mem[S0]的資料放入S1中
  3. 將S1存入 Mem[0x9c]的地方!!

每執行一次這個小程式,就可以讀出一筆Data!
假如需要連續讀取複數個Memory,是不是很方便啊!?

以下是實作的方式,由於內容有點複雜,有稍微修改一下,請參考以下內容(src/target/riscv/riscv-013.c的古早版本):

static int read_memory(struct target *target, target_addr_t address,
        uint32_t size, uint32_t count, uint8_t *buffer)
{
    ....省略

    struct riscv_program program;
    riscv_program_init(&program, target);
    riscv_addr_t r_data = riscv_program_alloc_w(&program);
    riscv_addr_t r_addr = riscv_program_alloc_x(&program);
    riscv_program_fence(&program);
    riscv_program_lx(&program, GDB_REGNO_S0, r_addr);
    switch (size) {
        case 1:
            riscv_program_lbr(&program, GDB_REGNO_S1, GDB_REGNO_S0, 0);
            break;
        case 2:
            riscv_program_lhr(&program, GDB_REGNO_S1, GDB_REGNO_S0, 0);
            break;
        case 4:
            riscv_program_lwr(&program, GDB_REGNO_S1, GDB_REGNO_S0, 0);
            break;
        default:
            LOG_ERROR("Unsupported size: %d", size);
            return ERROR_FAIL;
    }
    riscv_program_addi(&program, GDB_REGNO_S0, GDB_REGNO_S0, size);
    riscv_program_sw(&program, GDB_REGNO_S1, r_data);
    riscv_program_sx(&program, GDB_REGNO_S0, r_addr);

    /* The first round through the program's execution we use the regular
     * program execution mechanism. */
    switch (riscv_xlen(target)) {
    case 64:
        riscv_program_write_ram(&program, r_addr + 4, ((riscv_addr_t) address) >> 32);
    case 32:
        riscv_program_write_ram(&program, r_addr, (riscv_addr_t) address);
        break;
    default:
        LOG_ERROR("unknown XLEN %d", riscv_xlen(target));
        return ERROR_FAIL;
    }

    if (riscv_program_exec(&program, target) != ERROR_OK) {
        ....省略錯誤處理的部分
    }

    int d_data = (r_data - riscv_debug_buffer_addr(target)) / 4;
    int d_addr = (r_addr - riscv_debug_buffer_addr(target)) / 4;

    riscv_addr_t cur_addr = riscv_read_debug_buffer_x(target, d_addr);
    riscv_addr_t fin_addr = address + (count * size);
    while (cur_addr < fin_addr) {

        ...基本上就是重複以上流程,直到讀取完畢為止!
    
    }

    ....省略

中間過程落落長的,有興趣的讀者可以自行研究!

  
  

1.3 Write Memory

基本上同上一節的Read Memory,我們考慮以下範例:

lw      s1,0x9c(zero) 
sw      s1,0(s0)
addi    s0,s0,4
fence
ebreak

一樣假設$s0中負責存目標的Address,$s1中負責存放Data!
以上程式的流程如下:

  1. 將Mem[0x9c]的資料放入S1中
  2. 將S1存入 Mem[S0]的地方!!
  3. S0 = S0 + 4 //前進到下一個Word

以下是實作的方式,由於內容有點複雜,一樣只節錄部分內容,
請參考以下內容(src/target/riscv/riscv-013.c的古早版本):

static int write_memory(struct target *target, target_addr_t address,
        uint32_t size, uint32_t count, const uint8_t *buffer)
{
    ....省略

    struct riscv_program program;
    riscv_program_init(&program, target);
    riscv_addr_t r_data = riscv_program_alloc_w(&program);
    riscv_addr_t r_addr = riscv_program_alloc_x(&program);
    riscv_program_fence(&program);
    riscv_program_lx(&program, GDB_REGNO_S0, r_addr);
    riscv_program_lw(&program, GDB_REGNO_S1, r_data);

    switch (size) {
        case 1:
            riscv_program_sbr(&program, GDB_REGNO_S1, GDB_REGNO_S0, 0);
            break;
        case 2:
            riscv_program_shr(&program, GDB_REGNO_S1, GDB_REGNO_S0, 0);
            break;
        case 4:
            riscv_program_swr(&program, GDB_REGNO_S1, GDB_REGNO_S0, 0);
            break;
        default:
            LOG_ERROR("Unsupported size: %d", size);
            return ERROR_FAIL;
    }

    riscv_program_addi(&program, GDB_REGNO_S0, GDB_REGNO_S0, size);
    riscv_program_sx(&program, GDB_REGNO_S0, r_addr);

    ....省略

    if (riscv_program_exec(&program, target) != ERROR_OK) {
        ....省略錯誤處理的部分
    }

    /* The rest of this program is designed to be fast so it reads various
     * DMI registers directly. */
    int d_data = (r_data - riscv_debug_buffer_addr(target)) / 4;
    int d_addr = (r_addr - riscv_debug_buffer_addr(target)) / 4;

    riscv_addr_t cur_addr = 0xbadbeef;
    riscv_addr_t fin_addr = address + (count * size);
    LOG_DEBUG("writing until final address 0x%016" PRIx64, fin_addr);
    while ((cur_addr = riscv_read_debug_buffer_x(target, d_addr)) < fin_addr) {
        
        ...基本上就是重複以上流程,直到讀取完畢為止!

    }

    ....省略
}

  
  
  

2. Abstract Commands Overview

終於講完Program Buffer基本的操作!
以整體架構上來看,Program Buffer基本上就是萬能的,又可以在上面執行任意指令,
那又為啥需要提供Abstract Commands的功能!?

換個角度想一下,假如今天硬體能夠對特定功能把它抽象化,讓它們從一道道的指令,
變成一個預先定義好的Register,然後透過對這個Register讀寫的操作來執行動作,
是不是就能夠加速Debugger得到資料!!! (有沒有很厲害的設計!

以上節的例子來說,就是把Access(包含Read/Write) Register,
直接變成一道指令的話,那我們是不是可以省掉大量Program Buffer操作的時間!
  
  

2.1 Abstract Commands Format

首先先來定義一下這個指令的格式:

https://ithelp.ithome.com.tw/upload/images/20171227/20107327xeAGGUceie.png
---引用自RISC-V External Debug Support 0.13

這邊講解一下每個Field的意義:

  • cmdtype: 指定要執行的Abstract Command
    • 0: Access Register
    • 1: Qucik Access,後面還會再詳細介紹
  • control: 這部分依照上面的cmdtype的動作,而有不同的定義!
      
      

2.2 Access Register

這個Abstract Command的主要功能是用在Access(包含Read/Write) Register、Programm Buffer上

首先,一樣是先來看一下這個指令的格式:

https://ithelp.ithome.com.tw/upload/images/20171227/20107327UiRxfdsrJA.png
---引用自RISC-V External Debug Support 0.13

  • cmdtype: 這邊固定為0,表示要執行Access Register這個動作
  • size : 確定需要Access Register的長度
    • 2: 32 bits
    • 3: 64 bits
    • 4: 128 bits
  • postexec: 當設成1的時候,執行Program Buffer一次
  • transfer: 當設成1的時候,依照write的動作執行讀/寫
    • 用來保護Prboram Buffer執行的時候避免修改到Register!
  • write: 確認讀寫的方向
    • 0: 將指定Register中的值,複製到$data中
    • 1: 從$data中將其值,複製到指定的Register中
  • regno: 指定的Register的Number,可以參考下圖!

https://ithelp.ithome.com.tw/upload/images/20171227/20107327mxmG4ExEvA.png
---引用自RISC-V External Debug Support 0.13

因此,從上面的組合中,可以將這個Abstract Command提供的功能拆成以下幾個:

  1. write=0, transfer=1: 將指定Register中的值,複製到$data中
  2. write=1, transfer=1: 從$data中將其值,複製到指定的Register中
  3. postexec=1 : 執行Program Buffer

再過來就是實作的部分啦,請參考以下內容(src/target/riscv/riscv-013.c)

static int register_read_abstract(struct target *target, uint64_t *value,
        uint32_t number, unsigned size)
{
    RISCV013_INFO(info);

    /// 譯註: 目前這版只支援Access基本的CSRs
    if (number >= GDB_REGNO_FPR0 && number <= GDB_REGNO_FPR31 &&
            !info->abstract_read_fpr_supported)
        return ERROR_FAIL;
    if (number >= GDB_REGNO_CSR0 && number <= GDB_REGNO_CSR4095 &&
            !info->abstract_read_csr_supported)
        return ERROR_FAIL;

    uint32_t command = access_register_command(number, size,
            AC_ACCESS_REGISTER_TRANSFER);

    int result = execute_abstract_command(target, command);
    if (result != ERROR_OK) {
        if (info->cmderr == CMDERR_NOT_SUPPORTED) {
            if (number >= GDB_REGNO_FPR0 && number <= GDB_REGNO_FPR31) {
                info->abstract_read_fpr_supported = false;
                LOG_INFO("Disabling abstract command reads from FPRs.");
            } else if (number >= GDB_REGNO_CSR0 && number <= GDB_REGNO_CSR4095) {
                info->abstract_read_csr_supported = false;
                LOG_INFO("Disabling abstract command reads from CSRs.");
            }
        }
        return result;
    }

    if (value)
        *value = read_abstract_arg(target, 0);

    return ERROR_OK;
}

Register Read的部分也類似,請參考以下內容(src/target/riscv/riscv-013.c)

static int register_write_abstract(struct target *target, uint32_t number,
        uint64_t value, unsigned size)
{
    RISCV013_INFO(info);

    if (number >= GDB_REGNO_FPR0 && number <= GDB_REGNO_FPR31 &&
            !info->abstract_write_fpr_supported)
        return ERROR_FAIL;
    if (number >= GDB_REGNO_CSR0 && number <= GDB_REGNO_CSR4095 &&
            !info->abstract_write_csr_supported)
        return ERROR_FAIL;

    uint32_t command = access_register_command(number, size,
            AC_ACCESS_REGISTER_TRANSFER |
            AC_ACCESS_REGISTER_WRITE);

    if (write_abstract_arg(target, 0, value) != ERROR_OK) {
        return ERROR_FAIL;
    }

    int result = execute_abstract_command(target, command);
    if (result != ERROR_OK) {
        if (info->cmderr == CMDERR_NOT_SUPPORTED) {
            if (number >= GDB_REGNO_FPR0 && number <= GDB_REGNO_FPR31) {
                info->abstract_write_fpr_supported = false;
                LOG_INFO("Disabling abstract command writes to FPRs.");
            } else if (number >= GDB_REGNO_CSR0 && number <= GDB_REGNO_CSR4095) {
                info->abstract_write_csr_supported = false;
                LOG_INFO("Disabling abstract command writes to CSRs.");
            }
        }
        return result;
    }

    return ERROR_OK;
}

  
  

2.3 Quick Access

首先,一樣是先來看一下這個指令的格式:

https://ithelp.ithome.com.tw/upload/images/20171227/20107327gRFaiKTG6c.png
---引用自RISC-V External Debug Support 0.13

沒什麼好說的,cmdtype這邊固定為1!
主要提供的動作如下:

  1. 將Hart進入Haled的狀態
  2. 執行Program Buffer
  3. 讓Hart的狀態Resume

當Debugger無法在Hart的狀態是Runing下讀取資料時,可以利用Quick Access短暫的暫停Hart,然後執行預先寫好的Program Buffer取得所需的資料,在自動去Resume,避免長時間的將Hart停下!
不過由於目前這版還沒看到有使用到,這邊姑且只是翻譯一下Spec.的內容!
  
  
  

99. 結語

以上就是我覺得Debug Module精華的部分!
另外明天的主題將會深入介紹Debug Module中常用到的Registers,
應該比較繁瑣一些~~~XD
  
  
  

參考資料

  1. RISC-V External Debug Support 0.13
  2. GitHub: riscv/riscv-openocd

上一篇
Day 08: RISC-V Debug Module (上篇): Overview & Target Status Control
下一篇
Day 10: RISC-V Debug Module (下篇)-Debug Module Registers
系列文
系統架構秘辛:了解RISC-V 架構底層除錯器的秘密!30

尚未有邦友留言

立即登入留言