【问题标题】:How to output an integer to screen risc-v assembly如何将整数输出到屏幕 risc-v 程序集
【发布时间】:2021-04-04 13:02:19
【问题描述】:

以下是我用来尝试打印到控制台的程序集:

global _start

_start:
  addi   a0, x0, 1
  addi   a1, x0, 42
  addi   a7, x0, 63
  ecall

  addi   a0, x0, 0
  addi   a7, x0, 93
  ecall

.data
num:
  .byte 6  

我编译用

riscv64-unknown-elf-as  -o example.o  example.S
riscv64-unknown-elf-ld  -o example  example.o

并使用 spike代理内核

运行
spike pk example

没有生成输出。

这适用于https://www.kvakil.me/venus/

  addi   a0, x0, 1
  addi   a1, x0, 42
  ecall

并打印 42。

另外,如果我想在数据段中打印 num 的内容,我该怎么做呢?

【问题讨论】:

    标签: assembly system-calls riscv


    【解决方案1】:

    我根据 Peter Cordes 的回答管理了一个解决方案。我在这里发布实现以防有人需要它并供我自己参考。

    更新:

    步骤:

    1. 给定一个有符号数,取其绝对值,如果该数为负数, 通过变量记下它。
    2. 选择下一点对齐的结束地址位置。
    3. 执行重复划分并将提醒存储在适当的内存位置。
    4. 如果数字为负数,则在开头添加“-”。
    5. 通过从末尾减去第一个地址来获取长度。然后调用适当的系统调用。

    系统调用可以找到here

    C 代码在逻辑上反映程序集

    #include <unistd.h>
    
    void num_print(long num){
        unsigned int base = 10;
        int sign_bit = 0;
    
        char string[20];
        char* end = string + 19;
        char* p   = end;
        *p = '\n';
        
        if (num < 0){
            num = 0 - num;
            sign_bit = 1;
        }
    
        do {
            *(--p) = (num % base) + '0';
            num /= base;
        } while (num);
    
        if (sign_bit)
            *(--p) = '-';
        
        size_t len = end - p;
        write(1, p, len + 1);
    }
    
    int main(){
        int arr[3] = {1234567, -1234567, 0};
        for (int i=0; i < 3; i++){
            num_print(arr[i]);
        }
        return 0;
    }
    

    Risc-v 组装

    .global _start
    
    .text
    _start:
        la           s1, arr          # s1: load arr address
        addi         s2, zero, 3      # s2: arr length
    
        addi         sp, sp, -8       # push 1 item to stack 
        sd           ra, 0(sp)        # save return address
        mv           s3, zero         # s3: i loop counter  
        j            compare_ipos
    
    L1:
        slli         s4, s3, 3        # s4: i * 8
        add          s5, s1, s4       # s5: address of a[i]
        ld           a0, 0(s5)        # a0: arr[i]
        jal          ra, num_print    # call num_print
        addi         s3, s3, 1        # increment i
    
    compare_ipos:
        blt          s3, s2, L1       # loop if i < 3
        j            exit
     
    num_print:
        addi         sp, sp, -40      # create stack space
        sd           s0, 32(sp)       # store frame pointer
        addi         s0, sp, 40       # new frame pointer
      
        addi         t0, zero, 0      # initialize sign_bit
        addi         t1, zero, 10     # divisor and new-line char
        addi         t2, s0, -16      # t2: string[n] 
        add          a1, zero, t2     # a1: string[0] currently string[n]
      
        addi         t3, zero, '\n'   # '\n' char
        sb           t3, 0(a1)        # store '\n'
      
        bge          a0, zero, PVE    # if num >= 0 go to L1 else get absolute
        xori         a0, a0, -1       # (num ^ -1)
        addi         a0, a0, 1        # num + 1
        addi         t0, zero, 1      # set sign-bit to 1
    
    PVE:
        remu         t3, a0, t1       # num % 10
        addi         t3, t3, 48       # convert to ascii
        addi         a1, a1, -1       # decrement start pointer
        sb           t3, 0(a1)        # store value
        divu         a0, a0, t1       # num /= 10
        blt          zero, a0, PVE    # if num > 0 loop
    
        beq          t0, zero, print  # if sign_bit = 0 go to print else, add '-' char
        addi         t3, zero, '-'    # ascii '-'
        addi         a1, a1, -1       # decrement start pointer
        sb           t3, 0(a1)        # store '-'
    
    print:
        sub          t4, t2, a1       # t4: len -- string[n] - string[0]
        addi         a2, t4, 1        # len + 1
        addi         a0, zero, 1      # file descriptor to write to
        addi         a7, zero, 64     #  pk SYS_write
        ecall                         # transfer control to os
    
        ld           s0, 32(sp)       # restore frame pointer
        addi         sp, sp, 40       # restore stack pointer
    
        ret                           # return from function        
     
    exit:
        ld           ra, 0(sp)        # restore ra
        addi         sp, sp, 8        # pop stack
    
        addi         a0, zero, 0      # return value
        addi         a7, zero, 93     # syscall exit code
        ecall
    
    .data
    arr:
      .dword  12345670, -12345670, 0
    

    【讨论】:

    • 获取号码的位数——长度。需要计算内存偏移量。 - 不是真的,您可以从缓冲区的end 开始存储,如How do I print an integer in Assembly Level Programming without printf from the c library? 所示。完成后,您有一个指向数字开头的指针,无论它在哪里,您都可以将其传递给 `write(fd, buf, len)。您可以通过减去 end-start 来计算长度。
    • 两次除法很浪费;如果您需要数字字符串从内存中的特定位置开始(即您不只是要传递指向写入系统调用的指针),请在生成向后或右对齐到末尾后复制它们缓冲区。
    • 如果你真的要在num &lt; 0 上分支,用0 - num 否定它,即sub t0, zero, t0。如果你要避免分支,你只需要那个无分支的abs 实现,例如signbit = num&lt;0,例如slt。或者更好地使用sra t2, t0, 31 来获得 0 / 非零结果(您还希望将其作为 xor/sub 2 的补码标识 abs 技巧的掩码)。
    • 使用s0..2 作为临时寄存器很奇怪。这些 ABI 名称对应于调用约定:s 寄存器是“已保存”的,即,如果您完全修改它们,您应该保存/恢复它们,而 t 寄存器是您的调用者在您修改时不关心的临时变量。 (您退出系统调用是因为_start 不是一个函数,但是您不会从这里使用调用保留寄存器获得任何好处,因为您自己不进行任何调用。)
    • 您的 C 使用 printf("%s"),但您的 asm 使用 write(1, buf, len)。这很奇怪;你的 asm 浪费了几个指令 0-终止字符串。希望您实际上并未将'\0' 字节作为长度的一部分写入,因此内核不在乎它是否存在。所以你应该删除sb t6, 0(t5) # store null to address。 (另外,它是 ASCII NUL,而不是 null,并且没有理由将零复制到 t6,您可以完成 sb zero, 0(t5)。有时使用 zero,有时使用 x0。这些是 RISC 中的相同寄存器- V; 使用相同的名称以保持一致性。
    【解决方案2】:

    系统调用取决于环境。像 Venus 或 RARS 这样的“玩具”系统有自己的一套玩具系统调用,可以执行打印整数等操作。

    在像 GNU/Linux 这样的真实系统中,您可以使用 ecall 访问的真正系统调用只能将字节复制到文件描述符。如果要输出文本,则需要在用户空间的内存中创建文本,并将指针传递给 write 系统调用。

    Spike + pk 显然更像 Linux,具有 POSIX write(2) 系统调用,不像那些可以将整数直接传递给 print-int ecall 的玩具系统调用环境。 https://www.reddit.com/r/RISCV/comments/dagvzr/where_do_i_find_the_list_of_stdio_system_etc/ 有一些示例和链接。值得注意的是https://github.com/riscv/riscv-pk/blob/master/pk/syscall.h,我们在其中找到#define SYS_write 64 作为write 系统调用的调用号(进入a7)。

    write 系统调用采用 args:write(int fd, const void *buf, size_t count)

    将二进制整数格式化为 ASCII 字符串是 library 函数(如 printf)会做的事情。玩具系统没有库,所以它们只是将一些有用的函数作为系统调用。如果你想控制诸如前导零或填充到固定宽度之类的东西,你必须自己写。 但在像 Spike-pk 这样的系统上,您只有简单的类 Unix 系统调用并且(也许?)根本没有库,所以您必须始终自己做。

    仅使用 Linux / Unix / Spike-pk 系统调用,您需要重复除以 10 以获得二进制整数的十进制数字。就像在 How do I print an integer in Assembly Level Programming without printf from the c library? 中一样,它显示了适用于 Linux 的 C 和 x86-64 程序集:

    char *itoa_end(unsigned long val, char *p_end) {
      const unsigned base = 10;
      char *p = p_end;
      do {
        *--p = (val % base) + '0';
        val /= base;
      } while(val);                  // runs at least once to print '0' for val=0.
    
      // write(1, p,  p_end-p);
      return p;  // let the caller know where the leading digit is
    }
    

    转换为 RISC-V 程序集(或使用 gcc 或 clang 编译,例如通过 https://godbolt.org/)。在堆栈上保留一个小缓冲区很方便。

    另外,如果我想在数据段中打印 num 的内容,我该怎么做呢?

    lw把数字存入一个寄存器,然后做和上面一样的事情。

    【讨论】:

      猜你喜欢
      • 2020-05-05
      • 1970-01-01
      • 2012-12-21
      • 2012-10-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-12
      • 2011-04-18
      相关资源
      最近更新 更多