본문 바로가기
코딩일지/RISC-V

riscv-gnu-toolchain을 이용한 바이너리 -> hex code 변환 방법

by 퇴근희망1일차 2025. 1. 8.
반응형

2025.01.08. 초안 작성

 

이것도 일단 두서없이 작성

 

이후에 정리할 것임

 

2025.01.07 - [코딩일지/RISC-V] - riscv-gnu-toolchain 설치 관련 기록

 

위의 포스팅을 보고 일단 riscv-gnu-toolchain부터 설치하고 와야 함

 

 

아래는 decoder.py 프로그램

import os

def load_binary_to_memory(binary_file, memory_size, memory_base=0x0):
    """
    Reads a binary file and maps its contents into a memory array of the specified size.

    Args:
        binary_file (str): Path to the binary file to load.
        memory_size (int): Total size of the memory in bytes.
        memory_base (int): Base address of the memory (default: 0x0).

    Returns:
        list: Memory contents as a list of integers.
    """
    # Initialize memory with 0x00
    memory = [0x00] * memory_size

    try:
        # Open and read the binary file
        with open(binary_file, "rb") as f:
            binary_data = f.read()

        # Ensure binary data fits into the memory size
        binary_length = len(binary_data)
        if binary_length > memory_size:
            raise ValueError(f"Binary file size ({binary_length} bytes) exceeds memory size ({memory_size} bytes)")

        # Map binary data into memory, starting from the base address
        start_offset = memory_base
        memory[start_offset:start_offset + binary_length] = binary_data

    except FileNotFoundError:
        print(f"Error: File '{binary_file}' not found.")
    except Exception as e:
        print(f"Error: {e}")

    return memory

def save_memory_to_file(memory, output_file, word_size=4):
    """
    Saves memory contents to a file, formatted as words.

    Args:
        memory (list): Memory contents as a list of integers.
        output_file (str): Path to the output file.
        word_size (int): Size of each memory word in bytes (default: 4 bytes).
    """
    try:
        with open(output_file, "w") as f:
            for i in range(0, len(memory), word_size):
                # Group memory data into words and write as hex strings
                word = memory[i:i + word_size]
                word.reverse()  # Convert to little-endian representation
                f.write(f"{''.join(f'{byte:02X}' for byte in word)}\n")
        print(f"Memory saved to {output_file}")
    except Exception as e:
        print(f"Error: {e}")

# Example usage
if __name__ == "__main__":
    # Configuration
    binary_file = "program.bin"    # Path to the binary file (e.g., compiled from assembly)
    memory_size = 256             # Total memory size in bytes
    memory_base = 0x0             # Base address for loading the binary
    output_file = "memory.hex"    # Output file for memory contents

    # Load binary into memory
    memory = load_binary_to_memory(binary_file, memory_size, memory_base)

    # Save memory to a HEX file
    save_memory_to_file(memory, output_file)

 

 

아래는 sample.s 어셈블리 코드

.global _boot
.text

_boot:
    # Load variable_a and variable_b into registers
    lui x10, %hi(variable_a)       # variable_a 주소 로드
    addi x10, x10, %lo(variable_a)
    lw x8, 0(x10)                  # x8 = variable_a 값

    lui x10, %hi(variable_b)       # variable_b 주소 로드
    addi x10, x10, %lo(variable_b)
    lw x9, 0(x10)                  # x9 = variable_b 값

    # Add and store in variable_c
    add x5, x8, x9

    lui x10, %hi(variable_c)       # variable_c 주소 로드
    addi x10, x10, %lo(variable_c)
    sw x5, 0(x10)                  # variable_c에 x5 값 저장

    # UART base address load
    lui x1, 0x80010                # UART base address 로드
    addi x1, x1, 0

    # Output 32-bit result via UART
    jal x0, _uart_output           # UART로 첫 번째 바이트 출력
    jal x0, _uart_output           # UART로 두 번째 바이트 출력
    jal x0, _uart_output           # UART로 세 번째 바이트 출력
    jal x0, _uart_output           # UART로 네 번째 바이트 출력

    # Infinite loop (program end)
    _END:
        addi x0, x0, 0
        jal x0, _END

    _uart_output:
        # Ready check loop
    _uart_ready_check:
        lw x6, 0x08(x1)                # UART Ready 값 읽기
        andi x6, x6, 1                 # Ready 플래그 확인
        beq x6, x0, _uart_ready_check  # Ready가 0이면 대기

        sb x5, 0(x1)                   # 데이터 출력
        srai x5, x5, 8                 # 다음 바이트 준비
        ret                            # 루틴 종료

.data
variable_a:
    .word 0xDEAD0000

variable_b:
    .word 0x0000BEEF

variable_c:
    .word 0x00000000

 

 

어셈블리 코드가 하는 것은 간단함

어셈블리 코드의 목적은 아래와 같음

1. variable_c = variable_a + variable_b 연산 수행

2. UART TX로 variable_c 값을 출력

 

어셈블리 코드를 가지고 hex 코드로 변환하는 방법은 다음과 같음.

 

set path=([riscv-gnu-toolkit설치경로]/bin path)
riscv32-unknown-linux-gnu-as file.s -o file.o
riscv32-unknown-linux-gnu-objcopy -O binary file.o program.bin
python decoder.py

 

아래는 GPT 설명

 

  • riscv32-unknown-linux-gnu-as sample1.s -o sample1.o
    • RISC-V 어셈블러(as)를 사용하여 어셈블리 파일(sample1.s)을 **오브젝트 파일(sample1.o)**로 변환합니다.
    • 이 단계는 링커 스크립트를 사용하지 않습니다. 어셈블리 코드에서 .text, .data 등의 섹션을 정의하면 기본 설정에 따라 .o 파일로 컴파일됩니다.
    • 결과물: ELF 형식의 오브젝트 파일.
  • riscv32-unknown-linux-gnu-objcopy -O binary sample1.o program.bin
    • 오브젝트 파일(sample1.o)을 **바이너리 파일(program.bin)**로 변환합니다.
    • 이 명령어는 링커를 호출하지 않고, 단순히 ELF 형식의 오브젝트 파일에서 섹션을 추출하여 순수 바이너리 데이터를 생성합니다.
    • 결과물: 바이너리 파일(링커에 의해 메모리 배치가 결정되지 않음).

 

 

위의 명령어를 실행하고나면 memory.hex 파일이 생성됨

DEAD0000
0000BEEF
00000000
00000537
00050513
00052483
009402B3
00000537
00050513
00552023
800100B7
00008093
0180006F
0140006F
0100006F
00C0006F
00000013
FFDFF06F
0080A303
00137313
FE030CE3
00508023
4082D293
00008067
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

 

RISC-V는 boot address가 32'h0000_0000이라서 instruction이 32'h0000_0000부터 쌓여야함

지금 당장 봤을 때는 코드 섹션이랑 데이터 섹션이 반대가 되어야 할 것 같음.

역시 xcellium 시뮬레이션 돌려봤을 때 구동이 안됨.

링커스크립트 물려서 같이 돌려야 할 것 같음.

 

나의 경우 링커스크립트 아래와 같이 사용 중임

ASIC에서 RISC-V를 만드는 것이라 우선은 테스트를 위해 512b짜리 파운드리 SRAM 이용해서 코드를 구동시킴

그래서 메모리가 매우 적은 것을 볼 수 있음

 

linker.ld

/* Thanks https://github.com/darklife/darkriscv */
  __heap_size    = 0x20;  /* required amount of heap */
  __stack_size  = 0x20;  /* required amount of stack */

  MEMORY
  {
    ROM (rwx) : ORIGIN = 0x00000000, LENGTH = 0x00100
    RAM (rwx) : ORIGIN = 0x00000100, LENGTH = 0x00100
  }
  SECTIONS
  {
    .text :
    {
      *(.boot)
      *(.text)
      *(.text)
      *(.rodata*)
    } > ROM
    .data :
    {
      *(.sbss)
      *(.data)
      *(.bss)
      *(.rela*)
      *(COMMON)
    } > RAM

    .heap :
    {
      . = ALIGN(4);
      PROVIDE ( end = . );
      _sheap = .;
      . = . + __heap_size;
      . = ALIGN(4);
      _eheap = .;
    } >RAM

    .stack :
    {
      . = ALIGN(4);
      _estack = .;
      . = . + __stack_size;
      . = ALIGN(4);
      _sstack = .;
    } >RAM
  }

 

 

링커스크립트를 쓸 경우 스크립트

# Step 1: 어셈블리 -> 오브젝트 파일 생성
riscv32-unknown-linux-gnu-as sample1.s -o sample1.o

# Step 2: 링커를 사용하여 ELF 실행 파일 생성 (링커 스크립트 사용)
riscv32-unknown-linux-gnu-ld -T linker.ld sample1.o -o sample1.elf

# Step 3: ELF 실행 파일을 순수 바이너리로 변환
riscv32-unknown-linux-gnu-objcopy -O binary sample1.elf program.bin

# Step 4: 바이너리 파일을 읽어서 hexdecimal로 변환
python decoder.py

 

아래는 변환된 hexadecimal 프로그램임

10000513
00052403
10400513
00052483
009402B3
10800513
00552023
800100B7
00008093
0180006F
0140006F
0100006F
00C0006F
00000013
FFDFF06F
0080A303
00137313
FE030CE3
00508023
4082D293
00008067
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
DEAD0000
0000BEEF
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

 

바이너리를 hexadecimal 코드로 변환하는 것 자체는 정상적으로 수행되는 것 확인

전체 타이밍 다이어그램

 

위의 코드로 RISC-V 시뮬레이션을 돌리니까 돌긴 도는데 UART TX로 32'hDEAD_BEEF 값이 제대로 나오는지 검증 필요한 상황

 

 

프로그램 로드 이후 실제 동작

 

이것저것 체크해본 결과, assembly 코드로 작성된 ret이 00008067로 변환되어 문제가 발생했던 것 같음

 

hex code: 00008067

assembly code: jalr x0, 0(x1)

ret을 보면 x1 어드레스로 점프하는 건데 x1 address가 지금 UART base address로 되어있다는게 문제.

어셈블리어를 쓸 일이 없다보니 잘 몰랐는데 포인터 주소들이 벌써 할당된 의미가 있나봄

위키피디아 발췌본, https://en.wikipedia.org/wiki/RISC-V

 

다음과 같이 어셈블리 코드를 수정하였음

sample1.s

.global _boot
.text

_boot:
    # Load variable_a and variable_b into registers
    lui x10, %hi(variable_a)       # variable_a 주소 로드
    addi x10, x10, %lo(variable_a)
    lw x8, 0(x10)                  # x8 = variable_a 값

    lui x10, %hi(variable_b)       # variable_b 주소 로드
    addi x10, x10, %lo(variable_b)
    lw x9, 0(x10)                  # x9 = variable_b 값

    # Add and store in variable_c
    add x5, x8, x9

    lui x10, %hi(variable_c)       # variable_c 주소 로드
    addi x10, x10, %lo(variable_c)
    sw x5, 0(x10)                  # variable_c에 x5 값 저장

    # UART base address load
    lui x28, 0x80010                # UART base address 로드
    addi x28, x28, 0

    # Output 32-bit result via UART
    jal x1, _uart_output           # UART로 첫 번째 바이트 출력
    jal x1, _uart_output           # UART로 두 번째 바이트 출력
    jal x1, _uart_output           # UART로 세 번째 바이트 출력
    jal x1, _uart_output           # UART로 네 번째 바이트 출력

    # Infinite loop (program end)
    _END:
        addi x0, x0, 0
        jal x1, _END

    _uart_output:
        # Ready check loop
    _uart_ready_check:
        lw x6, 0x08(x28)                # UART Ready 값 읽기
        andi x6, x6, 1                 # Ready 플래그 확인
        beq x6, x0, _uart_ready_check  # Ready가 0이면 대기

        sb x5, 0(x28)                   # 데이터 출력
        srai x5, x5, 8                 # 다음 바이트 준비
        ret                            # 루틴 종료

.data
variable_a:
    .word 0xDEAD0000

variable_b:
    .word 0x0000BEEF

variable_c:
    .word 0x00000000

 

이후 동일한 과정을 거쳐서 hexadecimal로 프로그램을 뽑아서 시뮬레이션 다시 실행.

 

 

이번엔 정상적으로 도는 것 같다.

UART로 4 byte 전송 이후 프로그램이 END 루프에 들어왔다.

 

UART로 들어가는 write request data도 순서대로 0xEF, 0xBE, 0xAD, 0xDE로 다 합치면 0xDEAD_BEEF가 잘 나오는 것을 확인할 수 있었다.

반응형

'코딩일지 > RISC-V' 카테고리의 다른 글

RISC-V 개발 시 유용한 사이트 정리  (0) 2025.01.17
riscv-gnu-toolchain 설치 관련 기록  (0) 2025.01.07

댓글