C 코드 추가하기

2023년 3월 8일

main을 부르기 전에

앞 글에서는 어셈블리만 사용했는데, 이번에는 C 코드를 추가해봅시다. 모든 C 코드는 main에서 출발하니, 이전의 어셈블리 코드에서 마지막에 main을 호출하면 됩니다. 물론, 그 전에 할 일이 있죠.

레지스터 초기화

RISC-V는 C의 지역 변수와 전역 변수에 접근하기 위해 두 레지스터 sp(스택 포인터)와 gp(글로벌 포인터)를 사용합니다. C 함수를 호출하기 위해서는 둘 다 반드시 초기화되어 있어야 합니다. sp의 초기화는 다른 MCU와 마찬가지로 스택 영역의 끝 주소로 하면 됩니다. 별 일 없으면 RAM의 시작 주소와 동일할 겁니다. 문제는 gp입니다.

gp는 RISC-V의 특징적인 기능 중 하나로, 변수에 접근할 때 변수의 절대 주소 대신 gp에 대한 상대 주소로 접근할 수 있게 해줍니다. 절대 주소 접근법은 주소의 상위 20비트를 먼저 레지스터에 저장하고, 이 레지스터에 하위 12비트만큼의 오프셋을 더해 접근하는 두 단계를 거칩니다. 반면에 상대 주소 접근법은 gp에 12비트 오프셋을 더해 접근하는 한 단계만 거치면 됩니다. 단, 이 오프셋은 부호 있는 12비트 정수 상수이기 때문에 접근할 변수가 gp가 가리키는 주소를 기준으로 ±2 KiB 범위 내에 있어야 합니다.

따라서 링커는 변수의 주소와 gp의 값을 보고서 변수에 접근할 때 절대 주소로 접근할지, gp에 대한 상대 주소로 접근할지를 결정하는 최적화 단계를 거치는데, 이를 gp relaxation이라고 합니다. gp relxation을 사용하기 위해서는 링커에게 gp의 값을 미리 알려주어야 하며, 이는 링커 스크립트에 미리 약속된 심볼 __global_pointer$를 정의하여 해결할 수 있습니다. 링커는 이 심볼의 주소가 gp의 값이라고 가정하고 gp relxation을 수행한 것이기 때문에 gp를 처음에 __global_pointer$의 주소로 초기화하고 나서는 절대 값을 변경하면 안 됩니다.

섹션 초기화

이 부분은 여타 MCU와 동일합니다. .data 섹션은 플래시 메모리에서 값을 가져와 RAM에 복사하고, .bss 섹션은 RAM을 0으로 초기화합니다.

main 호출

이제 본격적으로 main을 호출할 시간입니다. RISC-V의 호출 규약(calling convention)에 따르면, 함수 호출 시 인자는 a0a7 레지스터에, 반환 값은 a0a1 레지스터를 통해 전달됩니다. 레지스터보다 두 배 큰 인자는 두 레지스터에 나눠서 전달하는데 이때에도 정렬 제한이 있어서 짝수번 레지스터와 그 다음 레지스터로 나눕니다. (즉, 인자가 a1a2에 나눠서 전달되지는 않습니다.) 반환값도 크기가 두 배라면 마찬가지로 나눠서 전달합니다. 또한, 레지스터만으로는 부족하거나 크기가 두 배보다 크다면 다른 레지스터가 아닌 다른 방식으로 전달합니다.

C 표준상 main은 두 인자 int argcchar** argv를 받습니다. 32비트 RISC-V 기준 int와 포인터 모두 레지스터와 동일하게 32비트이므로, argca0로, argva1로 전달됩니다.

32비트와 64비트 RISC-V에서의 타입별 크기
타입 RV32 RV64
char 1 1
short 2 2
int 4 4
long 4 8
long long 8 8
포인터 4 8

여기서는 따로 명령행 인자를 넣지 않을 것이기 때문에, argcargv를 0으로 초기화합니다. 즉, main을 호출하기 전에 a0a1에 0을 대입합니다.

구현

위에 따라 구현한 부트 코드는 다음과 같습니다.

gd32vf103.S

.section .start, "ax", %progbits

_start:
    /* Jump to reset_handler. */
    lui a0, %hi(reset_handler)
    jalr zero, %lo(reset_handler)(a0)

.globl reset_handler
.type reset_handler, %function
reset_handler:
    /* Set global pointer. */
    .option push
    .option norelax
    la gp, __global_pointer$
    .option pop

    /* Set initial stack pointer: end of the section .stack */
    la sp, _estack

    /* Load data section. */
    la a0, _sdata                           // a0 = &_sdata;
    la a1, _edata                           // a1 = &_edata;
    la a2, _sidata                          // a2 = &_sidata;
    load_data_loop:                         // while (1) {
        beq a0, a1, load_data_loop_end      //     if (a0 == a1) break;
        lw t0, 0(a2)                        //     t0 = *a2;
        sw t0, 0(a0)                        //     *a0 = t0;
        addi a0, a0, 4                      //     a0 += 4;
        addi a2, a2, 4                      //     a2 += 4;
        j load_data_loop                    // }
    load_data_loop_end:

    /* Clear bss section. */
    la a0, _sbss                            // a0 = &_sbss;
    la a1, _ebss                            // a1 = &_ebss;
    clear_bss_loop:                         // while (1) {
        beq a0, a1, clear_bss_loop_end      //     if (a0 == a1) break;
        sw zero, 0(a0)                      //     *a0 = zero;
        addi a0, a0, 4                      //     a0 += 4;
        j clear_bss_loop                    // }
    clear_bss_loop_end:

    /* Call main. */
    li a0, 0
    li a1, 0
    call main                               // main(0, 0);

전 글의 코드에서 reset_handler를 수정했습니다. 먼저 gpsp를 초기화한 뒤, .data 섹션을 4바이트 단위로 플래시 메모리에서 램으로 복사하고 .bss 섹션을 4바이트 단위로 0으로 초기화합니다. 마지막으로 main을 호출합니다.

gp를 초기화할 때에는 gp relaxation을 비활성화하는 norelax라는 옵션을 주었습니다. 링커 입장에선 gp의 값을 이미 __global_pointer$의 주소로 인식하기 때문에 위 코드와 같이 gp의 값을 초기화하면 초기화하는 명령어가 gp relaxation으로 최적화되어 사라집니다.gp relaxation을 적용한 상태에서 저 부분을 컴파일하면 아무 의미 없는 mv gp, gp가 됩니다. 따라서 이 경우엔 gp relaxation을 비활성화하는 것이 필요합니다.

나머지 부분의 초기화를 위한 _estack, _sdata 등의 심볼은 링커 스크립트에서 정의합니다.

gd32vf103.ld

OUTPUT_ARCH("riscv")

/* Entry address. */
start = 0x00000000;
/* Reserved stack size. */
_stack_size = 2K;

MEMORY
{
    FLASH(rxai!w) : ORIGIN = 0x08000000, LENGTH = 128K
    RAM(wxa!ri) : ORIGIN = 0x20000000, LENGTH = 32K
}

SECTIONS
{
    /* Flash sections. */

    .start :
    {
        KEEP(*(.start))
    } >FLASH

    .text :
    {
        *(.text .text.*)
    } >FLASH

    .rodata :
    {
        *(.rodata .rodata.*)
    } >FLASH

    /* RAM sections. */

    . = ALIGN(4);

    _sidata = .;
    .data : AT(_sidata)
    {
        _sdata = .;

        *(.data .data.*)

        . = ALIGN(8);
        PROVIDE(__global_pointer$ = . + 0x800);

        *(.sdata .sdata.*)
        *(.srodata .srodata.*)

        . = ALIGN(4);
        _edata = .;
    } >RAM

    .bss :
    {
        _sbss = .;

        *(.sbss .sbss.*)
        *(.bss .bss.*)
        *(COMMON)

        . = ALIGN(4);
        _ebss = .;
    } >RAM

    /* Reserve stack section. */
    .stack ORIGIN(RAM) + LENGTH(RAM) - _stack_size :
    {
        . = _stack_size;
        _estack = .;
    } >RAM
}

다른 MCU의 매우 기본적인 링커 스크립트와 비교하면 대단히 유사합니다만 __global_pointer$를 정의하는 부분과 .sdata, .srodata, .sbss 세 섹션이 추가되었습니다. 앞에서 언급했듯이 gp에 대한 상대 주소로 접근하는 것은 gp 기준 ±2 KiB 내라는 좁은 범위에서만 가능하므로 전역 변수 중 자주 사용되는 것부터 gp의 값과 가까운 주소에 배치하는 것이 유리합니다. 따라서 이런 변수들은 별도로 .sdata, .srodata, .sbss 세 섹션에 추가됩니다. 위 링커 스크립트는 세 섹션을 연속되게 배치하고, 이 부분의 시작 주소로부터 2 KiB 떨어진 곳을 gp의 값으로 설정하였습니다. 이렇게 하면 세 섹션이 너무 크지 않은 이상 모두 gp에 대한 상대 주소로 접근할 수 있습니다.

마지막으로 다음과 같이 main 함수를 정의합니다.

main.c

#include <stdint.h>

int main(void) {
    uint32_t a = 0xdeadbeef;

    volatile uint32_t cnt = 0;
    while (1) {
        cnt++;
    }
}

내용은 이전 글에서 reset_handler가 하던 것과 똑같습니다.

빌드 후 디버깅을 하면 다음과 같이 main이 실행되어 a가 초기화되고 cnt가 증가하는 모습을 볼 수 있습니다.


(gdb) load
Loading section .start, size 0x64 lma 0x8000000
Loading section .text, size 0x22 lma 0x8000064
Start address 0x00000000, load size 134
Transfer rate: 290 bytes/sec, 67 bytes/write.
(gdb) b main
Breakpoint 1 at 0x8000084: file main.c, line 8.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) c
Continuing.

Breakpoint 1, 0x08000084 in main () at main.c:8
8               cnt++;
(gdb) p/x a
$1 = 0xdeadbeef
(gdb) p cnt
$2 = 1
(gdb) c
Continuing.

Breakpoint 1, 0x08000084 in main () at main.c:8
8               cnt++;
(gdb) p cnt
$3 = 2

자세한 빌드 방법은 저장소의 Makefile을 참고하시기 바랍니다.

참고 문헌