- Arm

el0t_64_sync_handler  // arch/arm64/kernel/entry-common.c
   el0_da        // arch/arm64/kernel/entry-common.c
      do_mem_abort    // arch/arm64/mm/fault.c
         do_translation_fault // arch/arm64/mm/fault.c
            do_page_fault  // arch/arm64/mm/fault.c

asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
{
unsigned long esr = read_sysreg(esr_el1);

switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_SVC64:
el0_svc(regs);
break;
case ESR_ELx_EC_DABT_LOW:
el0_da(regs, esr); >>>
break;

static void noinstr el0_da(struct pt_regs *regs, unsigned long esr)
{
unsigned long far = read_sysreg(far_el1);

enter_from_user_mode(regs);
local_daif_restore(DAIF_PROCCTX);
do_mem_abort(far, esr, regs); >>>
exit_to_user_mode(regs);
}

void do_mem_abort(unsigned long far, unsigned long esr, struct pt_regs *regs)
{
        const struct fault_info *inf = esr_to_fault_info(esr);
        unsigned long addr = untagged_addr(far);

        if (!inf->fn(far, esr, regs))  >>>
                return; 

        if (!user_mode(regs))
                die_kernel_fault(inf->name, addr, esr, regs);

(where)
do_translation_fault function is registered in fault_info variable.

static const struct fault_info fault_info[] = {
        { do_bad,               SIGKILL, SI_KERNEL,     "ttbr address size fault"       },
        { do_bad,               SIGKILL, SI_KERNEL,     "level 1 address size fault"    },
        { do_bad,               SIGKILL, SI_KERNEL,     "level 2 address size fault"    },
        { do_bad,               SIGKILL, SI_KERNEL,     "level 3 address size fault"    },
        { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "level 0 translation fault"     },
        { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "level 1 translation fault"     },
        { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "level 2 translation fault"     },
        { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "level 3 translation fault"     },
        { do_page_fault,        SIGSEGV, SEGV_ACCERR,   "level 0 access flag fault"     },
        { do_page_fault,        SIGSEGV, SEGV_ACCERR,   "level 1 access flag fault"     },


static int __kprobes do_translation_fault(unsigned long far, 
                                          unsigned long esr,
                                          struct pt_regs *regs)
{
        unsigned long addr = untagged_addr(far);

        if (is_ttbr0_addr(addr))
                return do_page_fault(far, esr, regs); >>>

        do_bad_area(far, esr, regs);
        return 0;
}

- RISC-V

handle_exception
do_page_fault
handle_page_fault

arch/riscv/kernel/entry.S 
SYM_DATA_START_LOCAL(excp_vect_table)
RISCV_PTR do_trap_insn_misaligned
ALT_INSN_FAULT(RISCV_PTR do_trap_insn_fault)
RISCV_PTR do_trap_insn_illegal
RISCV_PTR do_trap_break
RISCV_PTR do_trap_load_misaligned
RISCV_PTR do_trap_load_fault
RISCV_PTR do_trap_store_misaligned
RISCV_PTR do_trap_store_fault
RISCV_PTR do_trap_ecall_u /* system call */
RISCV_PTR do_trap_ecall_s
RISCV_PTR do_trap_unknown
RISCV_PTR do_trap_ecall_m
/* instruction page fault */
ALT_PAGE_FAULT(RISCV_PTR do_page_fault)
RISCV_PTR do_page_fault   /* load page fault */  >>>
RISCV_PTR do_trap_unknown
RISCV_PTR do_page_fault   /* store page fault */  >>>
SYM_DATA_END_LABEL(excp_vect_table, SYM_L_LOCAL, excp_vect_table_end)

asmlinkage __visible noinstr void do_page_fault(struct pt_regs *regs)
{
irqentry_state_t state = irqentry_enter(regs);

handle_page_fault(regs);

local_irq_disable();

irqentry_exit(regs, state);
}

static __always_inline bool is_ttbr0_addr(unsigned long addr)
{
/* entry assembly clears tags for TTBR0 addrs */
return addr < TASK_SIZE;
}

RISC-V를 분석하면 가장 이해하기 어려운 콤포넌트는 opensbi이다. opensbi는 슈퍼바이저 모드와 
머신 모드의 인터페이스라고 할 수 있으며, 리눅스 커널의 가장 낮은 소프트웨어 레이어라고 할 수 있다.

리눅스 커널에서 RISC-V에 dependent한 동작을 정확히 이해하려면, 역시나 opensbi를 이해해야 한다.

이번 포스트에서는 opensbi 스펙 중에 marchid를 중심으로 소프트웨어의 실행 흐름을 정리한다.
ChatGPT이 이런 내용을 알려주지 않으니 많은 개발자에게 도움이 됐으면 좋겠다.

RISC-V 단체에서 기술한 opensbi 스펙 문서를 보자.

4.6. Function: Get machine architecture ID (FID #5)
struct sbiret sbi_get_marchid(void);

https://lists.riscv.org/g/tech-brs/attachment/361/0/riscv-sbi.pdf#:~:text=Function:%20Firmware%20Features%20Set%20(FID%20%230)%20struct,for%20which%20per%20feature%20supported%20values%20are

marchid를 읽어오는 인터페이스이다. 리눅스 커널에서 marchid에 대한 opensbi는 어떻게 구현됐을까?

다음 함수를 보자.

rch/riscv/kernel/sbi.c
long sbi_get_marchid(void)
{
return __sbi_base_ecall(SBI_EXT_BASE_GET_MARCHID);
}

어셈블리 명령어로 보면 sbi_get_marchid 함수의 구현부는 아래와 같다:

      SP:FFFFFFFF8000B3BA|sbi_get_marchid:   c.addi     sp,-0x10      ; sp,-16
      SP:FFFFFFFF8000B3BC|                   c.sdsp     s0,0x8(sp)    ; s0,8(sp)
      SP:FFFFFFFF8000B3BE|                   c.addi4spn s0,0x10       ; s0,16
      SP:FFFFFFFF8000B3C0|                   c.li       a0,0x0
      SP:FFFFFFFF8000B3C2|                   c.li       a1,0x0
      SP:FFFFFFFF8000B3C4|                   c.li       a2,0x0
      SP:FFFFFFFF8000B3C6|                   c.li       a3,0x0
      SP:FFFFFFFF8000B3C8|                   c.li       a4,0x0
      SP:FFFFFFFF8000B3CA|                   c.li       a5,0x0
      SP:FFFFFFFF8000B3CC|                   c.li       a6,0x5
      SP:FFFFFFFF8000B3CE|                   c.li       a7,0x10       ; a7,16
      SP:FFFFFFFF8000B3D0|                   ecall

a7 레지스터에 0x10을 로딩한 다음에 ecall 명령어를 실행한다. 이 명령어를 실행하면 바로
머신 모드로 트랩이 유발된다.

이제부터는 opensbi 코드 분석이다.

00000000000003c8 <_trap_handler>:
     3c8:       34021273                csrrw   tp,mscratch,tp
     3cc:       06523023                sd      t0,96(tp) # 60 <_try_lottery+0x36>
     3d0:       300022f3                csrr    t0,mstatus
[...]
     468:       3002b073                csrc    mstatus,t0
     46c:       00010533                add     a0,sp,zero
     470:       16e0c0ef                jal     ra,c5de <sbi_trap_handler>

리눅스 커널에서 ecall 명령어를 실행하면, 트랩이 유발되면서 _trap_handler 레이블의 시작 주소로 점프한다.
그 다음에 sbi_trap_handler 함수로 분기한다.

참고로 opensbi에서 트랩 핸들러를 설정하는 루틴은 아래와 같다:

opensbi/firmware/fw_base.S
        /* Setup trap handler */
        lla     a4, _trap_handler
        csrr    a5, CSR_MISA
        srli    a5, a5, ('H' - 'A')
        andi    a5, a5, 0x1
        beq     a5, zero, _skip_trap_handler_hyp
        lla     a4, _trap_handler_hyp
_skip_trap_handler_hyp:
        csrw    CSR_MTVEC, a4

'csrw    CSR_MTVEC, a4'이 핵심 명령어이다. 


다시 원래 주제로 돌아와서 sbi_trap_handler() 함수를 분석하자.
함수의 분석 내용은 주석을 참고하자.

struct sbi_trap_context *sbi_trap_handler(struct sbi_trap_context *tcntx)
{
        int rc = SBI_ENOTSUPP;
        const char *msg = "trap handler failed";
        struct sbi_scratch *scratch = sbi_scratch_thishart_ptr();
        const struct sbi_trap_info *trap = &tcntx->trap;
        struct sbi_trap_regs *regs = &tcntx->regs;
        ulong mcause = tcntx->trap.cause;

        /* Update trap context pointer */
        tcntx->prev_context = sbi_trap_get_context(scratch);
        sbi_trap_set_context(scratch, tcntx);

        /* Austin: mcause 레지스터의 최상위 비트가 1인지를 체크한다. 만약 true이면 트렙의
                       종류는 인터럽트이다. */
        if (mcause & MCAUSE_IRQ_MASK) {
                if (sbi_hart_has_extension(sbi_scratch_thishart_ptr(),
                                           SBI_HART_EXT_SMAIA))
                        rc = sbi_trap_aia_irq();
                else
                        rc = sbi_trap_nonaia_irq(mcause & ~MCAUSE_IRQ_MASK);
                msg = "unhandled local interrupt";
                goto trap_done;
        }

        /* Austin: mcause 레지스터는 익셉션 코드 정보를 저장한다. 이 값에 따라 다른 방식으로
                      트랩을 처리한다. mcause는 Armv8-A의 esr_el3에 대응된다(개념적으로)  */
       switch (mcause) {
        case CAUSE_ILLEGAL_INSTRUCTION:
                rc  = sbi_illegal_insn_handler(tcntx);
                msg = "illegal instruction handler failed";
                break;
        case CAUSE_MISALIGNED_LOAD:
                sbi_pmu_ctr_incr_fw(SBI_PMU_FW_MISALIGNED_LOAD);
                rc  = sbi_misaligned_load_handler(tcntx);
                msg = "misaligned load handler failed";
                break;
        case CAUSE_MISALIGNED_STORE:
                sbi_pmu_ctr_incr_fw(SBI_PMU_FW_MISALIGNED_STORE);
                rc  = sbi_misaligned_store_handler(tcntx);
                msg = "misaligned store handler failed";
                break;
        case CAUSE_SUPERVISOR_ECALL:
        case CAUSE_MACHINE_ECALL:
               */ Austin: 리눅스 커널에서 ecall을 실행했으니, 이 루틴으로 분기한다 */
                rc  = sbi_ecall_handler(tcntx);
                msg = "ecall handler failed";
                break;
        case CAUSE_LOAD_ACCESS:
                sbi_pmu_ctr_incr_fw(SBI_PMU_FW_ACCESS_LOAD);
                rc  = sbi_load_access_handler(tcntx);
                msg = "load fault handler failed";
                break;

sbi_ecall_handler() 함수를 분석하자. 분석 내용은 주석을 참고하자.

lib/sbi/sbi_ecall.c
int sbi_ecall_handler(struct sbi_trap_context *tcntx)
{
        int ret = 0;
        struct sbi_trap_regs *regs = &tcntx->regs;
        struct sbi_ecall_extension *ext;
        unsigned long extension_id = regs->a7;
        unsigned long func_id = regs->a6;
        struct sbi_ecall_return out = {0};
        bool is_0_1_spec = 0;

        ext = sbi_ecall_find_extension(extension_id);

위 루틴에서 regs->a7은 0x10이고 regs->a6이다. 왜냐면, 리눅스 커널에서 ecall 명령어를 실행하기 직전에
a6와 a7 레지스터를 다음과 같이 지정했기 때문이다.

      SP:FFFFFFFF8000B3BA|sbi_get_marchid:   c.addi     sp,-0x10      ; sp,-16
      [...]
      SP:FFFFFFFF8000B3CC|                   c.li       a6,0x5
      SP:FFFFFFFF8000B3CE|                   c.li       a7,0x10       ; a7,16

아래 코드를 보면 extension_id는 base(0x10)이고 func_id는 0x5라는 사실을 알 수 있다. 

opensbi/include/sbi/sbi_ecall_interface.h
/* SBI function IDs for BASE extension*/
#define SBI_EXT_BASE_GET_SPEC_VERSION           0x0
#define SBI_EXT_BASE_GET_IMP_ID                 0x1
#define SBI_EXT_BASE_GET_IMP_VERSION            0x2
#define SBI_EXT_BASE_PROBE_EXT                  0x3
#define SBI_EXT_BASE_GET_MVENDORID              0x4
#define SBI_EXT_BASE_GET_MARCHID                0x5

sbi_ecall_handler() 함수의 아랫 부분 코드를 더 분석하자.

lib/sbi/sbi_ecall.c
int sbi_ecall_handler(struct sbi_trap_context *tcntx)
{
        int ret = 0;
        struct sbi_trap_regs *regs = &tcntx->regs;
        struct sbi_ecall_extension *ext;
        unsigned long extension_id = regs->a7;
        unsigned long func_id = regs->a6;
        struct sbi_ecall_return out = {0};
        bool is_0_1_spec = 0;

        ext = sbi_ecall_find_extension(extension_id);
        if (ext && ext->handle) {
*/ Austin: 'ext->handle' 구문에서 sbi_ecall_base_handler 함수가 호출된다. */
                ret = ext->handle(extension_id, func_id, regs, &out);
                if (extension_id >= SBI_EXT_0_1_SET_TIMER &&
                    extension_id <= SBI_EXT_0_1_SHUTDOWN)
                        is_0_1_spec = 1;
        } else {
                ret = SBI_ENOTSUPP;
        }

그 이유는 extension_id가 0x10이면 sbi_ecall_base_handler 함수가 호출되도록 등록했기 때문이다.
sbi_ecall_base_handler 함수가 호출되는 세세한 구현 방식은 나중에 설명하자.

opensbi/lib/sbi/sbi_ecall_base.c
static int sbi_ecall_base_register_extensions(void)
{
        return sbi_ecall_register_extension(&ecall_base);
}

struct sbi_ecall_extension ecall_base = {
        .name                   = "base",
        .extid_start            = SBI_EXT_BASE,
        .extid_end              = SBI_EXT_BASE,
        .register_extensions    = sbi_ecall_base_register_extensions,
        .handle                 = sbi_ecall_base_handler,
};

sbi_ecall_base_handler 함수이다.

opensbi/lib/sbi/sbi_ecall_base.c
static int sbi_ecall_base_handler(unsigned long extid, unsigned long funcid,
                                  struct sbi_trap_regs *regs,
                                  struct sbi_ecall_return *out)
{
        int ret = 0;

        switch (funcid) {
[...]
       case SBI_EXT_BASE_GET_MARCHID:
                out->value = csr_read(CSR_MARCHID);
                break;

'csr_read(CSR_MARCHID);' 매크로 함수의 리턴 값을 out-value에 저장한다.
'csr_read(CSR_MARCHID);'  구문의 정체는 무엇일까? 바로 marchid CSRs 레지스터이다.

   126f8:       f12027f3                csrr    a5,marchid
   126fc:       4501                    li      a0,0
   126fe:       e69c                    sd      a5,8(a3)

a3이 out의 주소를 저장하니, 'sd      a5,8(a3)' 명령어를 실행하면 marchid 레지스터의 값이 저장된다.

marchid 레지스터의 값이 어떻게 저장되는지 확인하자. sbi_ecall_handler 함수의 구현부이다.

int sbi_ecall_handler(struct sbi_trap_context *tcntx)
{
        int ret = 0;
        struct sbi_trap_regs *regs = &tcntx->regs;
[...]
       ext = sbi_ecall_find_extension(extension_id);
        if (ext && ext->handle) {
                ret = ext->handle(extension_id, func_id, regs, &out);
[...]
               if (!is_0_1_spec)
                        regs->a1 = out.value;

marchid CSR은 a1 레지스터를 통해서 저장된다. 'regs->a1' 가장 마지막 라인을 보자.


머신 모드에서 슈퍼바이저 모드(리눅스 커널)로 리턴하는 동작

이제 머신 모드에서 슈퍼바이저 모드로 리턴하는 동작이다. 즉 opensbi -> 리눅스 커널이다.

00000000000003c8 <_trap_handler>:
     3c8:       34021273                csrrw   tp,mscratch,tp
     3cc:       06523023                sd      t0,96(tp) # 60 <_try_lottery+0x36>
     3d0:       300022f3                csrr    t0,mstatus
[...]
    46c:       00010533                add     a0,sp,zero
     470:       16e0c0ef                jal     ra,c5de <sbi_trap_handler>

     ; sbi_trap_handler 함수 실행이 마무리되면 아래 어셈블리 명령어가 실행된다.
     ; 스택에 푸시한 (리눅스 커널에서 ecall을 실행하기 직전의) 레지스터를 로딩한다.
     ; 슈퍼바이저 모드(리눅스 커널)로 돌아가기 위해서이다.  
     474:       00853083                ld      ra,8(a0)
     478:       01053103                ld      sp,16(a0)
     47c:       01853183                ld      gp,24(a0)
     480:       02053203                ld      tp,32(a0)
     484:       03053303                ld      t1,48(a0)
     488:       03853383                ld      t2,56(a0)
     48c:       6120                    ld      s0,64(a0)
     48e:       6524                    ld      s1,72(a0)
     490:       6d2c                    ld      a1,88(a0)
     492:       7130                    ld      a2,96(a0)
[...]
     4de:       30029073                csrw    mstatus,t0 ; Austin: 되돌아갈 privilege 모드를 mstatus 레지스터 설정
     4e2:       10053283                ld      t0,256(a0)
     4e6:       34129073                csrw    mepc,t0  ; Austin: 되돌아갈 리눅스 커널의 주소 - ecall을 실행한 다음 주소
     4ea:       02853283                ld      t0,40(a0)
     4ea:       02853283                ld      t0,40(a0)
     4ee:       6928                    ld      a0,80(a0)
     4f0:       30200073                mret     ; Austin: 리눅스 커널로 리턴(슈퍼바이저 모드) 


      SP:FFFFFFFF8000B3BA|sbi_get_marchid:   c.addi     sp,-0x10      ; sp,-16
      SP:FFFFFFFF8000B3BC|                   c.sdsp     s0,0x8(sp)    ; s0,8(sp)
      SP:FFFFFFFF8000B3BE|                   c.addi4spn s0,0x10       ; s0,16
      SP:FFFFFFFF8000B3C0|                   c.li       a0,0x0
      SP:FFFFFFFF8000B3C2|                   c.li       a1,0x0
      SP:FFFFFFFF8000B3C4|                   c.li       a2,0x0
      SP:FFFFFFFF8000B3C6|                   c.li       a3,0x0
      SP:FFFFFFFF8000B3C8|                   c.li       a4,0x0
      SP:FFFFFFFF8000B3CA|                   c.li       a5,0x0
      SP:FFFFFFFF8000B3CC|                   c.li       a6,0x5
      SP:FFFFFFFF8000B3CE|                   c.li       a7,0x10       ; a7,16
      SP:FFFFFFFF8000B3D0|                   ecall
      SP:FFFFFFFF8000B3D4|                   c.bnez     a0,0xFFFFFFFF8000B3DE
      SP:FFFFFFFF8000B3D6|                   c.mv       a0,a1 ; Austin: 리턴값을 a0 레지스터에 복사한다. 그 이유는: RISC-V에서 리턴값은
                                                                               ; a0에 저장하기 때문.
      SP:FFFFFFFF8000B3D8|                   c.ldsp     s0,0x8(sp)    ; s0,8(sp)
      SP:FFFFFFFF8000B3DA|                   c.addi     sp,0x10       ; sp,16
      SP:FFFFFFFF8000B3DC|                   c.jr       ra ; Austin: caller 함수로 리턴한다.


아래는 위 어셈블리 명령어에 대응되는 커널 코드이다. 

arch/riscv/kernel/sbi_ecall.c
long __sbi_base_ecall(int fid)
{
struct sbiret ret;

ret = sbi_ecall(SBI_EXT_BASE, fid, 0, 0, 0, 0, 0, 0);
if (!ret.error)
return ret.value;
else
return sbi_err_map_linux_errno(ret.error);
}
EXPORT_SYMBOL(__sbi_base_ecall);

정리

콜 스택을 정리하자:

[리눅스 커널] - supervisor mode
sbi_get_marchid
 - __sbi_base_ecall
  -  ecall 
--------------------------
[opensbi]
  - _trap_handler
    -  sbi_trap_handler 
      - sbi_ecall_handler 
       - sbi_ecall_base_handler
  - a1에 리턴 값 저장    
 - mret     

PS: * 글이 도움이 됐으면 '좋아요'를 눌러주시면 좋겠습니다.

RISC-V에서 QEMU를 설정하고 빌드하는 방법은 아래 링크를 참고하자.

https://lore.kernel.org/all/20251112-v5_user_cfi_series-v23-0-b55691eacf4f@rivosinc.com/

How to test this series
=======================

Toolchain
---------
$ git clone git@github.com:sifive/riscv-gnu-toolchain.git -b cfi-dev
$ riscv-gnu-toolchain/configure --prefix=<path-to-where-to-build> --with-arch=rv64gc_zicfilp_zicfiss --enable-linux --disable-gdb  --with-extra-multilib-test="rv64gc_zicfilp_zicfiss-lp64d:-static"
$ make -j$(nproc)

Qemu
----
Get the lastest qemu
$ cd qemu
$ mkdir build
$ cd build
$ ../configure --target-list=riscv64-softmmu
$ make -j$(nproc)

Opensbi
-------
$ git clone git@github.com:deepak0414/opensbi.git -b v6_cfi_spec_split_opensbi
$ make CROSS_COMPILE=<your riscv toolchain> -j$(nproc) PLATFORM=generic

Linux
-----
Running defconfig is fine. CFI is enabled by default if the toolchain
supports it.

$ make ARCH=riscv CROSS_COMPILE=<path-to-cfi-riscv-gnu-toolchain>/build/bin/riscv64-unknown-linux-gnu- -j$(nproc) defconfig
$ make ARCH=riscv CROSS_COMPILE=<path-to-cfi-riscv-gnu-toolchain>/build/bin/riscv64-unknown-linux-gnu- -j$(nproc)

Running
-------

Modify your qemu command to have:
-bios <path-to-cfi-opensbi>/build/platform/generic/firmware/fw_dynamic.bin
-cpu rv64,zicfilp=true,zicfiss=true,zimop=true,zcmop=true

References
==========
[1] - https://github.com/riscv/riscv-cfi
[2] - https://lore.kernel.org/all/20240814081126.956287-1-samuel.holland@sifive.com/
[3] - https://lwn.net/Articles/889475/
[4] - https://developer.arm.com/documentation/109576/0100/Branch-Target-Identification
[5] - https://www.intel.com/content/dam/develop/external/us/en/documents/catc17-introduction-intel-cet-844137.pdf
[6] - https://lwn.net/Articles/940403/ 

 

Shadow stacks for 64-bit Arm systems

Return-oriented programming (ROP) has, for some years now, been a valuable tool for those who w [...]

lwn.net

 

 

RISC-V의 핵심은 opensbi이다. 어느 RISC-V 문서를 봐도 opensbi를 확인할 수 있다.
부트로더에서 opensbi가 실행되고, 리눅스 커널이 실행될 때도 opensbi에 접근한다.

이번 포스트에서는 opensbi를 빌드하는 방법에 대해 기술한다.

opensbi는 오픈 소스로 관리되며 위치는 아래와 같다.

https://github.com/riscv-software-src/opensbi

아래 명령어로 소스를 내려 받자.

$ git clone https://github.com/riscv-software-src/opensbi

빌드하기 전에 미리 아래와 같은 유틸리티를 설치할 필요가 있다. 

$ sudo apt install gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

opensbi 빌드 방법

opensbi를 빌드하는 핵심 명령어는 아래와 같다. 

make O=$OUTPUT CROSS_COMPILE=riscv64-linux-gnu- PLATFORM=generic 

계속 CROSS_COMPILE을 명령어로 하기 귀찮으니, 빌드 셸 스크립트(./build_opensbi.sh)를 생성해서 실행하자.

아래는 ./build_opensbi.sh의 구현부이다.

#!/bin/bash

echo "build opensbi"
TOP_PATH=$( cd "$(dirname "$0")" ; pwd )
OUTPUT="$TOP_PATH/out-riscv64"

BUILD_LOG="$TOP_PATH/riscv-build_log.txt"

build_start_time=`date +%s`

#echo "RPi build start" > $BUILD_LOG
#echo "Build start : $build_start_time" >> $BUILD_LOG

echo "open Build start : $build_start_time"

OUTPUT_PATH=$( cd "$(dirname "$0")" ; pwd )
OUTPUT="$OUTPUT_PATH/out-opensbi"

pushd opensbi > /dev/null

make O=$OUTPUT CROSS_COMPILE=riscv64-linux-gnu- PLATFORM=generic -j16  2>&1
popd > /dev/null

아래는 build_opensbi.sh 셸 스크립트를 실행할 때의 아웃풋이다.

$ ./build_opensbi.sh
build opensbi
open Build start : 1768870118
Loaded configuration '/home/austin/riscv_src/package_opensbi/deepack_open_sbi/opensbi/platform/generic/configs/defconfig'
Configuration saved to '/home/austin/riscv_src/package_opensbi/deepack_open_sbi/out-opensbi/platform/generic/kconfig/.config'
 CPP-DEP   platform/generic/firmware/fw_payload.elf.dep
 CPP-DEP   platform/generic/firmware/fw_jump.elf.dep
 CPP-DEP   platform/generic/firmware/fw_dynamic.elf.dep
[...]
 AR        lib/libsbi.a
 ELF       platform/generic/firmware/payloads/test.elf
 ELF       platform/generic/firmware/fw_dynamic.elf
 ELF       platform/generic/firmware/fw_jump.elf
 OBJCOPY   platform/generic/firmware/payloads/test.bin
 AS        platform/generic/firmware/fw_payload.o
 OBJCOPY   platform/generic/firmware/fw_dynamic.bin
 ELF       platform/generic/firmware/fw_payload.elf
 OBJCOPY   platform/generic/firmware/fw_jump.bin
 OBJCOPY   platform/generic/firmware/fw_payload.bin

빌드가 되면 다양한 오브젝트 파일이 생성되는데 핵심 파일은 fw_dynamic.bin이다.
fw_dynamic.bin 파일은 디버깅 정보가 없으니, fw_dynamic.elf 파일을 찾아서 어셈블리 명령어를 추출하자.

$ riscv64-linux-gnu-objdump -d fw_dynamic.elf  > assembly_opensbi.c

이제부터 어셈블리 명령어를 분석할 수 있다.

0000000000000000 <_fw_start>:
       0:       00050433                add     s0,a0,zero
       4:       000584b3                add     s1,a1,zero
       8:       00060933                add     s2,a2,zero
       c:       66c000ef                jal     ra,678 <fw_boot_hart>
[...]
00000000000003c8 <_trap_handler>:
     3c8:       34021273                csrrw   tp,mscratch,tp
     3cc:       06523023                sd      t0,96(tp) # 60 <_try_lottery+0x36>
     3d0:       300022f3                csrr    t0,mstatus
[...]
     46c:       00010533                add     a0,sp,zero
     470:       16e0c0ef                jal     ra,c5de <sbi_trap_handler>


이제부터 전처리 파일을 추출해보자.

전처리 파일(preprocessed file)은 매크로를 전부 파싱해서 보여주므로,
처음 코드를 분석할 때 유용하다.

opensbi에서 전처리 파일을 추출하려면 아래와 같은 패치를 생성하자.

diff --git a/Makefile b/Makefile
index 46541063..85e0f685 100644
--- a/Makefile
+++ b/Makefile
@@ -381,6 +381,7 @@ CFLAGS              =       -g -Wall -Werror -ffreestanding -nostdlib -fno-stack-protector -fno-st
 CFLAGS         +=      -fno-omit-frame-pointer -fno-optimize-sibling-calls
 CFLAGS         +=      -fno-asynchronous-unwind-tables -fno-unwind-tables
 CFLAGS         +=      -std=gnu11
+CFLAGS          +=      -save-temps=obj
 CFLAGS         +=      $(REPRODUCIBLE_FLAGS)
 # Optionally supported flags
 ifeq ($(CC_SUPPORT_VECTOR),y)

CFLAGS에 -save-temps=obj 옵션을 추가하는 코드이다.

전처리 파일과 C 코드에서 확인한 루틴을 비교하자.
sbi_list_del() 함수 in C 소스 파일:

include/sbi/sbi_list.h
static inline void sbi_list_del(struct sbi_dlist *entry)
{
        __sbi_list_del(entry->prev, entry->next);
        entry->next = (void *)SBI_LIST_POISON_NEXT;
        entry->prev = (void *)SBI_LIST_POISON_PREV;
}

sbi_list_del() 함수 in 전처리 소스 파일: SBI_LIST_POISON_NEXT 매크로가
0xFADEBABE으로 파싱되어서 출력된다.

static inline void sbi_list_del(struct sbi_dlist *entry)
{
 __sbi_list_del(entry->prev, entry->next);
 entry->next = (void *)0xFADEBABE;
 entry->prev = (void *)0xDEADBEEF;
}

RISC-V에서 QEMU를 설정하고 빌드하는 방법은 아래 링크를 참고하자.

https://lore.kernel.org/all/20251112-v5_user_cfi_series-v23-0-b55691eacf4f@rivosinc.com/

How to test this series
=======================

Toolchain
---------
$ git clone git@github.com:sifive/riscv-gnu-toolchain.git -b cfi-dev
$ riscv-gnu-toolchain/configure --prefix=<path-to-where-to-build> --with-arch=rv64gc_zicfilp_zicfiss --enable-linux --disable-gdb  --with-extra-multilib-test="rv64gc_zicfilp_zicfiss-lp64d:-static"
$ make -j$(nproc)

Qemu
----
Get the lastest qemu
$ cd qemu
$ mkdir build
$ cd build
$ ../configure --target-list=riscv64-softmmu
$ make -j$(nproc)

Opensbi
-------
$ git clone git@github.com:deepak0414/opensbi.git -b v6_cfi_spec_split_opensbi
$ make CROSS_COMPILE=<your riscv toolchain> -j$(nproc) PLATFORM=generic

Linux
-----
Running defconfig is fine. CFI is enabled by default if the toolchain
supports it.

$ make ARCH=riscv CROSS_COMPILE=<path-to-cfi-riscv-gnu-toolchain>/build/bin/riscv64-unknown-linux-gnu- -j$(nproc) defconfig
$ make ARCH=riscv CROSS_COMPILE=<path-to-cfi-riscv-gnu-toolchain>/build/bin/riscv64-unknown-linux-gnu- -j$(nproc)

Running
-------

Modify your qemu command to have:
-bios <path-to-cfi-opensbi>/build/platform/generic/firmware/fw_dynamic.bin
-cpu rv64,zicfilp=true,zicfiss=true,zimop=true,zcmop=true

References
==========
[1] - https://github.com/riscv/riscv-cfi
[2] - https://lore.kernel.org/all/20240814081126.956287-1-samuel.holland@sifive.com/
[3] - https://lwn.net/Articles/889475/
[4] - https://developer.arm.com/documentation/109576/0100/Branch-Target-Identification
[5] - https://www.intel.com/content/dam/develop/external/us/en/documents/catc17-introduction-intel-cet-844137.pdf
[6] - https://lwn.net/Articles/940403/

https://www2.eecs.berkeley.edu/Pubs/TechRpts/2015/EECS-2015-49.pdf

Any level of PTE may be a leaf PTE, so in addition to 4 KiB pages, Sv39 supports 2 MiB megapages
and 1 GiB gigapages, each of which must be virtually and physically aligned to a boundary equal
to its size.

https://www.reddit.com/r/RISCV/comments/v895do/what_are_super_pages_wrt_page_tables/?tl=ko

crash> vtop 0000003fbe7c33f8
VIRTUAL           PHYSICAL
3fbe7c33f8        12c4d53f8

PAGE DIRECTORY: ffffffd8c09e4000
  PGD: ffffffd8c09e47f0 => 417f5801  // 0001 [non-leaf]
  PMD: 0000000105fd6000 => 000000004d32a801 // 0001 [non-leaf]
  PTE: 134caa000 => 4b1354d7
 PAGE: 000000012c4d5000

crash> vtop 0xffffffff813f0cb0
VIRTUAL           PHYSICAL
ffffffff813f0cb0  415f0cb0

PAGE DIRECTORY: ffffffff81c4f000
  PGD: ffffffff81c4fff0 => 4fffe801   // 0001 [non-leaf]
  PMD: 000000013fffa000 => 00000000105000e3  // 0011 [leaf], 2MB page
  PTE: 41400000 => ffffffff80a29ce2

     PTE         PAGE: 0000000000000000 not present

오늘날 많은 신문과 블로그에서 RISC-V를 이야기합니다.
RISC-V는 오픈 소스 CPU 아키텍처이며, 누구나 라이선스 비용 없이 사용할 수 있습니다.

실제로 많은 **마이크로컨트롤러(MCU)**에서 이미 RISC-V를 사용합니다. 특히 단순하고 저비용이며 복잡한 소프트웨어가 필요 없는 장치에서 많이 사용됩니다. 하지만 중요한 질문은 이것입니다: RISC-V가 Cortex-A53이나 Cortex-A57 같은 Arm 프로세서를 대신할 수 있을까?

현재까지의 답은 아직 아니다입니다.
스마트폰과 자동차 분야에서는 Cortex-A7x, Cortex-X2와 같은 고성능 Arm 프로세서가 여전히 주류입니다.
RISC-V는 빠르게 성장하고 있지만, 고성능 시장에서는 아직 강하지 않습니다.

 

1. RISC-V에 대한 오해: “무료다”

RISC-V는 오픈 소스이고 로열티가 없습니다. 하지만 이것이 기업에게 완전히 무료라는 뜻은 아닙니다. 기업은 RISC-V 전문가를 고용해야 하고, 연구개발(R&D) 에 투자해야 합니다. Arm 프로세서를 사용할 때는 Arm이란 회사에서 지원과 문서를 받을 수 있습니다. 하지만 RISC-V에서는 스스로 문제를 해결해야 합니다.

이 때문에 많은 칩셋 업체들은 RISC-V를 프로토타입이나 데모 용도로만 사용하고, 최종 제품에는 잘 사용하지 않습니다.


2. Arm과 비교한 RISC-V 생태계


RISC-V 생태계는 아직 Arm에 비해 매우 젊습니다. Arm은 수십 년 동안 발전해 왔습니다. SiFive 같은 회사는 좋은 성과를 내고 있으며, 특히 리눅스 커널에 패치를 적용하고 있습니다. 하지만 많은 개발 도구와 라이브러리는 아직 충분히 준비되지 않았습니다.

RISC-V가 Arm과 같은 성숙한 단계에 이르려면 더 많은 시간이 필요합니다.

 

3. 많은 임베디드 개발자가 RISC-V를 배우지 않는 이유

저는 Arm 아키텍처에 익숙한 임베디드 개발자들을 자주 만납니다.
제가 RISC-V 세미나를 진행하면, 그들은 보통 듣기만 하고 깊이 배우려고 하지는 않습니다.

그들이 말하는 이유는 보통 이렇습니다: 

  • “새로운 아키텍처를 공부할 시간이 없습니다.”
  • “우리는 Arm 지식만으로도 프로젝트를 끝낼 수 있습니다.”


즉, 그들은 RISC-V 학습이 당장 필요하지 않다고 생각합니다.


마무리


저의 경력 대부분은 Arm 프로세서와 함께했습니다. 하지만 저는 미래에 Arm과 RISC-V 모두를 이해하는 개발자가 필요하다고 믿습니다. Arm은 여전히 모바일과 자동차에서 강력합니다.

RISC-V는 IoT, MCU, 실험적인 프로젝트에서 빠르게 성장하고 있습니다.

임베디드 개발자로 성장하고 싶다면, 지금부터 RISC-V를 공부해 두는 것이 좋습니다.
아직 산업 표준은 아니지만, Arm과 RISC-V를 모두 다룰 수 있다면 글로벌 취업 시장에서 더 경쟁력 있는 개발자가 될 수 있습니다.

리눅스 커널에서는 로컬 인터럽트(local interrupt)를 활성화하거나 비활성화할 때 사용하는 API 함수들이 있습니다:

 - local_irq_enable()
 -  local_irq_disable()

코드 분석: preempt_schedule_irq() 함수


이제 preempt_schedule_irq() 함수의 일부 코드를 살펴보겠습니다:

asmlinkage __visible void __sched preempt_schedule_irq(void)
{
	enum ctx_state prev_state;

	/* Catch callers which need to be fixed */
	BUG_ON(preempt_count() || !irqs_disabled());

	prev_state = exception_enter();

	do {
		preempt_disable();
		local_irq_enable();
		__schedule(SM_PREEMPT);
		local_irq_disable();
		sched_preempt_enable_no_resched();
	} while (need_resched());

	exception_exit(prev_state);
}

 

이 코드에서는 __schedule() 함수가 실행되는 동안 local_irq_enable() 함수가 호출되어 로컬 인터럽트를 활성화합니다.
그 후, __schedule() 함수가 종료되면 다시 local_irq_disable() 함수가 호출되어 로컬 인터럽트를 비활성화합니다.

디스어셈블리(Disassembly) 결과 분석


이제 이 코드를 RISC-V 기반 컴파일러로 빌드한 다음, 디스어셈블리(disassembly) 결과를 확인해 보겠습니다:

SP:FFFFFFFF80D8E526|preempt_schedule_irq:  lw         a5,0x8(tp)    ; a5,8(tp)  
SP:FFFFFFFF80D8E52A| c.bnez     a5,0xFFFFFFFF80D8E55E  
[...]  
SP:FFFFFFFF80D8E53C| csrsi      sstatus,0x2  
SP:FFFFFFFF80D8E540| c.li       a0,0x1  
SP:FFFFFFFF80D8E542| auipc      ra,0xFFFFF    ; ra,1048575  
SP:FFFFFFFF80D8E546| jalr       ra,0x516(ra)   ; ra,1302(ra) ; __schedule  
SP:FFFFFFFF80D8E54A| csrci      sstatus,0x2


이 코드를 보면 csrsi sstatus,0x2 명령어와 csrci sstatus,0x2 명령어가 사용됩니다.
이 명령어들은 각각 sstatus 레지스터의 2번째 비트를 설정(1)하거나 해제(0)하는 역할을 수행합니다. 
 
local_irq_enable() 함수의 내부 구현

 

local_irq_enable() 함수는 아키텍처에 의존적인 함수입니다. 코드를 단계별로 분석하면서 함수 내부 동작을 파악해 보겠습니다.

먼저 local_irq_enable() 함수의 구현부를 보겠습니다:

// include/linux/irqflags.h  
#define local_irq_enable() do { raw_local_irq_enable(); } while (0)


이 함수는 raw_local_irq_enable() 함수로 치환됩니다.

raw_local_irq_enable() 함수를 봅시다:

// include/linux/irqflags.h  
#define raw_local_irq_enable() arch_local_irq_enable()

 

이제 arch_local_irq_enable() 함수로 치환됩니다. 리눅스 커널에서는 매크로를 사용해 특정 함수가 아키텍처에 맞춰 동작하도록 하는 방식이 자주 사용됩니다.

arch_local_irq_enable() 함수의 구현부를 보겠습니다.

// arch/riscv/include/asm/irqflags.h  
static inline void arch_local_irq_enable(void)  
{  
	csr_set(CSR_STATUS, SR_IE);  
}

 

이 함수는 RISC-V 아키텍처에 맞춰 구현된 함수입니다. 여기서 SSTATUS 레지스터의 SR_IE 비트를 1로 설정하는 동작입니다. 

SR_IE 매크로와 SR_SIE 매크로 정의를 알아 봅시다.

// arch/riscv/include/asm/csr.h  
#define SR_IE  SR_SIE  
[...]  
#define SR_SIE _AC(0x00000002, UL)  /* Supervisor Interrupt Enable */


여기서 SR_IE는 0x2 값과 동일합니다. 이는 csrsi sstatus,0x2 명령어가 sstatus 레지스터의 2번째 비트를 설정한다는 의미입니다.

분석 결과 요약

 

분석한 내용을 정리하면 다음과 같습니다:

 - local_irq_enable() 함수는 로컬 인터럽트를 활성화하는 역할을 합니다.
-  RISC-V 아키텍처 기반에서는 csrsi sstatus,0x2 명령어가 해당 함수의 핵심 동작입니다.
-  이 명령어는 sstatus 레지스터의 2번째 비트를 1로 설정합니다.

하드웨어 디버깅: sstatus 레지스터 확인

 

아래는 하드웨어 디버깅 과정에서 확인한 결과입니다.

csrsi sstatus,0x2 명령어 실행 후 결과:


sstatus 레지스터 값이 0x2로 변경되었습니다.

csrci sstatus,0x2 명령어 실행 후 결과:


sstatus 레지스터 값이 0x0으로 변경되었습니다.

디버깅 결과로, 이제 local_irq_enable() 함수의 동작 원리와 sstatus 레지스터의 역할에 대해 명확히 이해했을 것입니다.
이를 통해 로컬 인터럽트를 활성화하고 비활성화하는 과정이 소프트웨어와 하드웨어 레벨에서 어떻게 연동되는지도 알 수 있습니다. 

RISC-V 아키텍처에서는 MMU를 지원하며, 멀티 레벨 페이지 변환을 지원합니다.

리눅스와 같은 Rich한 운영체제는 가상 메모리 시스템에서 실행되며, 소프트웨어가 바라보는
주소는 가상 주소입니다.

이 중에 1st lookup page table의 베이스 주소는 satp 레지스터를 사용해서 관리합니다.

상세한 동작 방식은 아래 강의 영상을 참고하세요:

 

 

RISC-V 아키텍처에서는 2가지 타입의 인터럽트 콘트롤러가 있다:

  • PLIC (Platform-level Interrupt Controller)
  • CLIC (Core-local Interrupt Controller)

아래 그림을 보면서 전체 구조를 확인하자:



PLIC는 외부 퍼리퍼럴 디바이스에서 유발되는 인터럽트를 받아서, RISC-V Hart에 전달(Routing)하는 역할을 한다. RISC-V Hart(Core)는 인터럽트가 유발됐다는 사실을 확인한 후, 익셉션을 유발한다.

CLIC는 RISC-V Hart 내부에 존재한다. 타이머나 Software generated interrupt를 유발하는데 주로 사용된다. CLIC를 통해서 인터럽트가 유발되면 RISC-V Hart는 역시 익셉션을 유발한다.

 

퍼리퍼럴에서 인터럽트가 유발되면 처리되는 실행 흐름을 알아보자:

 

 

[1]: 여러 퍼리퍼럴 디바이스에서 인터럽트를 유발한다. 여기서 말하는 인터럽트는 전기적인 신호이다.

[2]: PLIC 내부는 게이트웨이와 PLIC 코어로 구성된다. 게이트웨이가 물리적인 인터럽트 신호를 PLIC 내부에서 관리할 수 있도록 변환한 다음에, PLIC 코어에서 인터럽트를 RISC-V Hart에 라우팅한다.

[3]: RISC-V Hart는 익셉션을 유발한다. SCAUSE 레지스터의 가장 높은 비트를 1로 설정하고 익셉션 코드는 9로 설정한 다음에 익셉션 벡터 주소로 PC를 브랜치한다.

[4]: 인터럽트는 CPU 아키텍처에 의존적인 동작을 한다. 리눅스 커널은 CPU 아키텍처에 dependent하지 않는 소프트웨어 레이어를 가져간다. IRQ 서브 시스템에서 해당 인터럽트에 대한 정보(struct irq_desc)를 읽어서, 해당 인터럽트를 핸들링하는 인터럽트 핸들러 함수를 호출한다. 

[5]: 해당 인터럽트에 대응되는 인터럽트 핸들러가 호출된다. 예를 들어 rtc_starfive라는 인터럽트가 유발되면 rtc_starfive를 핸들링하는 인터럽트 핸들러 함수가 호출된다.



디바이스 드라이버를 개발하는 개발자들은 PLIC의 구조를 잘 알아야 할 필요가 있다. 대부분 퍼리퍼럴 디바이스는 인터럽트를 통해 하드웨어적인 변화를 알리기 때문이다.

 

상세한 동작 방식은 아래 강의 영상을 참고하세요:

 

 

 

Background

리눅스 커널은 함수와 자료 구조로 구성되어 있다. 자료 구조 중에서 가장 중요한 것은 무엇일까? 정답은 없지만, 많은 커널 개발자들은 task_struct 구조체라고 답할 것이다.

task_struct는 프로세스의 속성 정보를 나타내는 구조체이다. task_struct 구조체의 주소만 알면 커널의 대부분의 정보를 확인할 수 있다. 예를 들면:

-모든 프로세스의 task_struct 구조체 주소
-모든 프로세스의 콜 스택
-모든 프로세스별로 오픈한 파일 디스크립터 정보


그런데 이러한 task_struct 구조체가 범용 레지스터에서 확인될 수 있다면 어떤 생각이 들까? 아마 다음과 같을 것이다:

디버깅 관점: 디버깅을 매우 효율적으로 할 수 있겠다.
보안 관점: 시스템의 중요한 정보를 레지스터에서 확인할 수 있으므로 주의해야 한다.
RISC-V 아키텍처에서 커널의 tp 레지스터는 해당 프로세스의 task_struct 구조체의 주소를 저장한다. 이 내용을 보고 “정말 그럴까?”라는 생각이 들 수도 있다.


Debugging - task_struct

이제부터 RISC-V 디바이스에서 추출한 메모리 덤프를 분석해보자.
아래는 익셉션으로 인해 커널 크래시가 발생했을 때 출력된 커널 로그의 일부이다.

 

CPU1에서, PID가 1132인 bash 프로세스가 어떤 코드를 실행하다가 익셉션이 발생한 것을 확인할 수 있다.
우리가 주목해야 할 부분은 tp 레지스터가 ffffffd8c21be780 값을 포함하고 있다는 사실이다.

그렇다면 이번에는 Crash Utility에서 runq -m 명령어를 실행하여 각 코어에서 실행 중인 프로세스의 정보를 확인해 보자.

 

 


TASK: ffffffd8c21be780이라는 시그니처는 프로세스의 task_struct 주소를 나타낸다.
이 ffffffd8c21be780 주소가 정말 task_struct의 주소인지 확인하기 위해 다음과 같이 검증해 보자.



메모리를 확인해 보면 ffffffd8c21be780 주소가 실제로 task_struct 구조체의 시작 주소임을 알 수 있다.

Debugging - task_struct.thread

이번에는 또 다른 디버깅을 진행해 보자. 리눅스 커널에서 task_struct.thread 필드는 CPU 아키텍처에 의존적인 프로세스 정보를 저장한다.

 


이를 종합하면, task_struct.thread 필드의 타입은 struct thread_struct라는 것을 알 수 있다.
이제 task_struct.thread 정보를 Crash Utility로 확인해 보자.



task_struct.thread.s[3] 필드에서 ffffffd8c21be780 주소가 확인된다.


Summary

디버깅을 통해 다음과 같은 사실을 확인할 수 있다:

-tp 레지스터에는 task_struct의 주소가 저장되어 있다.
-task_struct.thread.s[3] 필드에도 task_struct의 주소가 저장되어 있다.

 

이 정보를 활용하면 더욱 효율적인 디버깅이 가능하다. 잘 참고하자.

배경

RISC-V 아키텍처에는 여러 가지 CSR(Control and Status Register) 레지스터가 있습니다.
그중 하나가 satp 레지스터입니다.

satp 레지스터의 역할은 무엇일까요? 첫 번째(1st) 페이지 테이블 엔트리의 주소를 저장하는 역할을 합니다.
만약 TRACE32 같은 디버깅 장비를 사용하면 satp 레지스터 값을 직접 확인할 수 있습니다. 하지만 TRACE32 없이 리눅스 커널 내부에서 satp 값을 확인하는 방법은 무엇일까요? 이 글에서는 그 방법에 대해서 상세히 다룹니다. 

리눅스 커널: swapper_pg_dir

satp 값(첫 번째 페이지 테이블의 엔트리 주소)을 확인하려면 swapper_pg_dir 전역 변수의 가상 주소를 먼저 알아야 합니다.
왜 swapper_pg_dir이 중요할까요? 
리눅스 커널이 부팅할 때, satp 레지스터의 값을 swapper_pg_dir에 저장하기 때문입니다.

 

Crash 유틸리티를 사용하여 swapper_pg_dir 주소 확인

 

 

Crash 유틸리티를 사용하면 swapper_pg_dir의 가상 주소와 물리 주소를 확인할 수 있습니다.

출력 결과:

- swapper_pg_dir 가상 주소: 0xffffffff81c4f000
- swapper_pg_dir 물리 주소: 0x41e4f000


satp 값 계산하기


swapper_pg_dir의 물리 주소를 이용해 satp 값을 계산할 수 있습니다.

0x41e4f000 >> 12 = 0x41e4f


왜 12비트를 오른쪽으로 이동할까요? 기본 페이지 크기(default page size)는 0x1000 (4KB)입니다. 물리 주소를 12비트(0x1000) 만큼 오른쪽으로 시프트하면 페이지 프레임 번호(PFN) 를 얻을 수 있습니다.

 

satp 레지스터 분석


더 정확한 정보를 확인하기 위해, RISC-V 명령어 매뉴얼(RISC-V Instruction Set Manual)의 satp 레지스터 비트 구조를 살펴보겠습니다.


satp 레지스터 비트 구조
-[43:0] 비트 → 페이지 프레임 번호(PFN) 저장 이 값이 root page table의 물리 주소입니다.
-[63:60] 비트 → 멀티 레벨 페이지 변환 단계 정보 저장
즉, satp 레지스터는 1st lookup page table의 베이스 주소를 저장합니다.

리눅스 커널 코드 분석


satp 값은 리눅스 커널에서 어떻게 설정될까요? 커널 부팅 시 실행되는 setup_vm_final() 함수에서 설정됩니다.

riscv/mm/init.c
static void __init setup_vm_final(void)
{
    ...
    csr_write(CSR_SATP, PFN_DOWN(__pa_symbol(swapper_pg_dir)) | satp_mode);

 


이 한 줄의 코드에는 중요한 정보가 포함되어 있습니다.

-__pa_symbol(swapper_pg_dir): swapper_pg_dir의 가상 주소를 물리 주소로 변환하는 매크로
-PFN_DOWN(): 물리 주소를 페이지 프레임 번호(PFN)로 변환하는 매크로
-satp_mode: 페이지 변환 레벨을 지정하는 값


satp_mode 값

 

satp_mode 값은 satp 레지스터의 [63:60] 비트에 저장됩니다.
RISC-V 문서와 리눅스 커널 코드에서 확인할 수 있습니다.

#define SATP_MODE_39 _AC(0x8000000000000000, UL)
#define SATP_MODE_48 _AC(0x9000000000000000, UL)
#define SATP_MODE_57 _AC(0xa000000000000000, UL)

 

요약

1. satp 레지스터는 첫 번째 페이지 테이블 엔트리 주소를 저장합니다.
리눅스 커널이 부팅될 때, satp 값이 swapper_pg_dir에 저장됩니다.

 

2. satp 값을 찾으려면:
swapper_pg_dir의 물리 주소를 확인합니다.
12비트 오른쪽 시프트(>> 12) 하여 페이지 프레임 번호(PFN)를 얻습니다.

 

3. setup_vm_final() 함수에서 satp 값이 설정됩니다.
satp_mode는 페이지 변환 레벨을 결정합니다.

 

이 과정을 이해하면, TRACE32 같은 외부 디버깅 장비 없이 리눅스 커널 내부에서 satp 값을 확인할 수 있습니다. 🚀

 

RISC-V 프로세서 기반 위에서 리눅스 커널이 실행될 때, 리눅스 커널 크래시가 발생하는 과정을 상세히 설명하는 강의 컨텐츠입니다. 보드를 브링업하는 과정에서 가장 고생하는 이슈가 크래시입니다. 이 영상을 보시고 고생을 덜 하시길 바랍니다.

강의 링크:

 

 

RISC-V 아키텍처에서 익셉션 코드(Exception codes)는 익셉션을 식별하는데 사용되는 중요한 정보입니다. 이번 강의 영상에서는 RISC-V 아키텍처의 익셉션 코드를 설명합니다.

 

강의 링크:

 

 

 

 

실전 프로젝트에서는 익셉션은 메모리 어보트라고 간주합니다. RISC-V 아키텍처에서 메모리 어보트 성 익셉션의 실행 흐름에 대해 설명하는 컨텐츠입니다.

 

강의 링크:

 

 

 

 

RISC-V 아키텍처에서 인터럽트는 익셉션의 한 종류로 처리합니다. 이는 Arm 아키텍처(Armv8-A, Armv7-A)와 비슷합니다. RISC-V 아키텍처에서 인터럽트 타입 익셉션에 대해 소개하는 컨텐츠입니다.

 

강의 링크:

 

 

RISC-V 아키텍처에서 인터럽트는 익셉션의 한 종류로 처리합니다. 이는 Arm 아키텍처(Armv8-A, Armv7-A)와 비슷합니다. RISC-V 아키텍처에서 인터럽트 타입 익셉션의 실행 흐름에 대해 설명하는 컨텐츠입니다.

 

관련 강의:

 

 

 

RISC-V 아키텍처에서 익셉션(Exception)의 기본 동작 원리를 설명하는 콘텐츠입니다. 익셉션이 유발되면 익셉션의 종류 별로 정의된 주소로 프로그램 카운터로 분기합니다.

 

관련 강의 영상:

 

 

#RISC-V
#Exception
#익셉션
#risc-v 

 

 

 

RISC-V 아키텍처에서 익셉션(Exception)의 동작 방식을 분석하기 앞서서, 익셉션의 종류를 파악할 필요가 있습니다. 이번 컨텐츠에서는 RISC-V 아키텍처에서 익셉션의 종류를 설명합니다. 

 

관련 영상:

 

 

#RISC-V

#Exception

#익셉션

#risc-v 

 

startup 코드 code-walkthrough

_start 심벌에서 스타트업 코드가 확인된다. Trap vector entry 주소를 설정한다.

0000000040200000 <_start>:
    40200000:   822a                    mv      tp,a0
    40200002:   84ae                    mv      s1,a1
    40200004:   00000193                li      gp,0
    40200008:   00074297                auipc   t0,0x74
    4020000c:   4402b283                ld      t0,1088(t0) # 40274448 <trap_entry+0x7352c>
    40200010:   10529073                csrw    stvec,t0
    40200014:   10401073                csrw    sie,zero

u-boot에서 익셉션이 유발되면 trap_entry 주소로 RISC-V Hart는 프로그램 카운터를 점프할 것이다.

이번에는 trap_entry 심벌의 코드를 보자.

0000000040200f1c <trap_entry>:
    40200f1c:   7111                    add     sp,sp,-256
    40200f1e:   e406                    sd      ra,8(sp)
    40200f20:   e80a                    sd      sp,16(sp)
    40200f22:   ec0e                    sd      gp,24(sp)
    40200f24:   f012                    sd      tp,32(sp)
...
   40200f5c:   14202573                csrr    a0,scause
    40200f60:   141025f3                csrr    a1,sepc
    40200f64:   14302673                csrr    a2,stval
    40200f68:   868a                    mv      a3,sp
    40200f6a:   398000ef                jal     40201302 <handle_trap>
    40200f6e:   14151073                csrw    sepc,a0
    40200f72:   60a2                    ld      ra,8(sp)
    40200f74:   61e2                    ld      gp,24(sp)
    40200f76:   7202                    ld      tp,32(sp)
    40200f78:   72a2                    ld      t0,40(sp)

위 코드에서 하는 일은 3가지다:

 1. General-purpose register를 스택에 저장한다 
 2. a0에 scause a1에 sepc 그리고 a2에 stval를 저장한다 
 3. handle_trap 함수를 호출한다 

위 어셈블리 루틴에 대응되는 *.S 소스 파일은 mtrap.S 이며, 위치는 아래와 같다: 

arch/riscv/cpu/mtrap.S
trap_entry:
        addi sp, sp, -32 * REGBYTES
        SREG x1,   1 * REGBYTES(sp)
        SREG x2,   2 * REGBYTES(sp)
        SREG x3,   3 * REGBYTES(sp)
        SREG x4,   4 * REGBYTES(sp)
...
       mv a3, sp
        jal handle_trap
        csrw MODE_PREFIX(epc), a0

        LREG x1,   1 * REGBYTES(sp)
        LREG x3,   3 * REGBYTES(sp)

Exception handler: handle_trap 함수 code-walkthrough

handle_trap 함수의 구현부는 아래와 같다.

arch/riscv/lib/interrupts.c
ulong handle_trap(ulong cause, ulong epc, ulong tval, struct pt_regs *regs)
{
        ulong is_irq, irq;

        /* An UEFI application may have changed gd. Restore U-Boot's gd. */
        efi_restore_gd();

        if (cause == CAUSE_BREAKPOINT &&
            CONFIG_IS_ENABLED(SEMIHOSTING_FALLBACK)) {
                ulong pre_addr = epc - 4, post_addr = epc + 4;
...
       }

        is_irq = (cause & MCAUSE_INT);
        irq = (cause & ~MCAUSE_INT);

        if (is_irq) {
                switch (irq) {
                case IRQ_M_EXT:
                case IRQ_S_EXT:
                        external_interrupt(0);  /* handle external interrupt */
                        break;
                case IRQ_M_TIMER:
                case IRQ_S_TIMER:
                        timer_interrupt(0);     /* handle timer interrupt */
                        break;
                default:
                        _exit_trap(cause, epc, tval, regs);
                        break;
                };
        } else {
                _exit_trap(cause, epc, tval, regs);
        }

아래 코드는 브레이크 포인트를 설정했을 때 처리되는 루틴이다:

        if (cause == CAUSE_BREAKPOINT &&
            CONFIG_IS_ENABLED(SEMIHOSTING_FALLBACK)) {

scause 레지스터의 최상위 레지스터를 읽어서 인터럽트로 익셉션이 유발됐는지 체크한다: 

        is_irq = (cause & MCAUSE_INT);
        irq = (cause & ~MCAUSE_INT);

아래는 RISC-V 아키텍처에서 인터럽트를 처리하는 루틴이다. 

        if (is_irq) {
                switch (irq) {
                case IRQ_M_EXT:
                case IRQ_S_EXT:
                        external_interrupt(0);  /* handle external interrupt */
                        break;
                case IRQ_M_TIMER:
                case IRQ_S_TIMER:
                        timer_interrupt(0);     /* handle timer interrupt */
                        break;

아래는 is_irq가 true가 아닌 경우 처리되는 구문이다.

        } else {
                _exit_trap(cause, epc, tval, regs);
        }

_exit_trap 함수의 전체 소스 코드는 아래와 같다:

arch/riscv/lib/interrupts.c
static void _exit_trap(ulong code, ulong epc, ulong tval, struct pt_regs *regs)
{
        static const char * const exception_code[] = {
                "Instruction address misaligned",
                "Instruction access fault",
                "Illegal instruction",
                "Breakpoint",
                "Load address misaligned",
                "Load access fault",
                "Store/AMO address misaligned",
                "Store/AMO access fault",
                "Environment call from U-mode",
                "Environment call from S-mode",
                "Reserved",
                "Environment call from M-mode",
                "Instruction page fault",
                "Load page fault",
                "Reserved",
                "Store/AMO page fault",
        };

        if (code < ARRAY_SIZE(exception_code))
                printf("Unhandled exception: %s\n", exception_code[code]);
        else
                printf("Unhandled exception code: %ld\n", code);

        printf("EPC: " REG_FMT " RA: " REG_FMT " TVAL: " REG_FMT "\n",
               epc, regs->ra, tval);
        /* Print relocation adjustments, but only if gd is initialized */
        if (gd && gd->flags & GD_FLG_RELOC)
                printf("EPC: " REG_FMT " RA: " REG_FMT " reloc adjusted\n",
                       epc - gd->reloc_off, regs->ra - gd->reloc_off);

        show_regs(regs);
        show_code(epc);
        show_efi_loaded_images(epc);
        panic("\n");
}

위 함수의 구현부에서 중요한 부분을 보자. 아래는 익셉션 코드이다.
RISC-V 메뉴얼 문서를 보면 확인할 수 있다.

        static const char * const exception_code[] = {
                "Instruction address misaligned",
                "Instruction access fault",
                "Illegal instruction",
                "Breakpoint",
                "Load address misaligned",
                "Load access fault",
                "Store/AMO address misaligned",
                "Store/AMO access fault",
                "Environment call from U-mode",
                "Environment call from S-mode",
                "Reserved",
                "Environment call from M-mode",
                "Instruction page fault",
                "Load page fault",
                "Reserved",
                "Store/AMO page fault",
        };

UART 콘솔 로그로 에러 메시지를 출력해주는 코드다:

        if (code < ARRAY_SIZE(exception_code))
                printf("Unhandled exception: %s\n", exception_code[code]);
        else
                printf("Unhandled exception code: %ld\n", code);

        printf("EPC: " REG_FMT " RA: " REG_FMT " TVAL: " REG_FMT "\n",
               epc, regs->ra, tval);
        /* Print relocation adjustments, but only if gd is initialized */
        if (gd && gd->flags & GD_FLG_RELOC)
                printf("EPC: " REG_FMT " RA: " REG_FMT " reloc adjusted\n",
                       epc - gd->reloc_off, regs->ra - gd->reloc_off);

레지스터를 출력하고, 익셉션이 유발된 코드를 출력한 다음에 panic() 함수를 호출한다.
 
        show_regs(regs);
        show_code(epc);
        show_efi_loaded_images(epc);
        panic("\n");

u-boot 크로스 컴파일 (RISC-V)

ubuntu (x86) 시스템에서 u-boot를 크로스 컴파일 (RISC-V)을 할 것이다.
먼저 빌드에 필요한 유틸리티를 설치하자:

sudo apt-get install libncurses-dev libssl-dev bc flex bison make gcc gcc-riscv64-linux-gnu -y

아래와 같은 명령어를 사용해 uboot 소스를 다운로드하자:

$ git clone https://github.com/u-boot/u-boot.git -b u-boot-2023.07.y
Cloning into 'u-boot'...
...
Checking out files: 100% (19873/19873), done.

다운로드가 마무리되면 u-boot 디렉토리가 생성됐다는 사실을 알 수 있어. 

$ ls
u-boot
$ cd u-boot

이제 크로스 컴파일을 위한 환경 설정을 하자: 

$ export ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu-

starfive_visionfive2_defconfig 를 적용해서, 다음 명령어를 사용해서 빌드를 하자:

$ make starfive_visionfive2_defconfig
$ make -j4

많은 바이너리가 생성되는데, u-boot 파일이 심벌이 포함된 바이너리 파일이다:

$ ls
api           common     env       MAINTAINERS                        spl         u-boot-dtb.bin      u-boot.srec
arch          config.mk  examples  Makefile                           System.map  u-boot-dtb.img      u-boot.sym
bin           configs    fs        net                                test        u-boot.dtb.out
board         disk       include   post                               tools       u-boot.img
boot          doc        Kbuild    py                                 u-boot      u-boot.lds

아래 명령어를 사용해서 u-boot의 어셈블리 명령어를 추출하자: 

$ ./riscv64-unknown-linux-gnu-objdump -d u-boot > code_uboot.c

code_uboot.c 파일에서 추출된 어셈블리 명령어를 확인할 수 있다.

먼저 헤더 정보를 확인하자.

$ ./riscv64-unknown-linux-gnu-objdump -x u-boot | more
u-boot:     file format elf64-littleriscv
u-boot
architecture: riscv:rv64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0000000040200000

Program Header:
    LOAD off    0x0000000000001000 vaddr 0x0000000040200000 paddr 0x0000000040200000 align 2**12
         filesz 0x0000000000088298 memsz 0x000000000008ecb0 flags rwx
 DYNAMIC off    0x0000000000074e80 vaddr 0x0000000040273e80 paddr 0x0000000040273e80 align 2**3
         filesz 0x0000000000000110 memsz 0x0000000000000110 flags rw-
   STACK off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
         filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-

'start address 0x0000000040200000' information reveals that the startup code is located address at 0x40200000.

startup 코드 code-walkthrough

_start 심벌에서 스타트업 코드가 확인된다. Trap vector entry 주소를 설정한다.

0000000040200000 <_start>:
    40200000:   822a                    mv      tp,a0
    40200002:   84ae                    mv      s1,a1
    40200004:   00000193                li      gp,0
    40200008:   00074297                auipc   t0,0x74
    4020000c:   4402b283                ld      t0,1088(t0) # 40274448 <trap_entry+0x7352c>
    40200010:   10529073                csrw    stvec,t0
    40200014:   10401073                csrw    sie,zero

RISC-V에서 Privilege Mode는 프로세서에서 실행되는 소프트웨어가 시스템 자원에 대해 얼마나 많은 제어와 접근을 가질 수 있는지를 결정합니다. Privilege Mode는 보안을 구현하고, 서로 다른 소프트웨어를 분리하며, 사용자 수준의 응용 프로그램이 운영 체제나 하드웨어에 직접적으로 간섭하지 않도록 보장하는 데 매우 중요합니다.

RISC-V Privilege Mode
RISC-V는 여러 가지 Privilege Mode를 정의하며, 각각은 소프트웨어 스택의 다른 계층을 위해 설계되었습니다:

 


사용자 모드 (U-mode):

목적: 이 모드는 가장 낮은 특권 수준으로, 주로 사용자 응용 프로그램을 실행하는 데 사용됩니다. 이 모드는 중요한 시스템 자원에 대한 접근을 제한하여 사용자 프로그램이 하드웨어나 민감한 시스템 설정에 직접 접근하지 못하도록 합니다.
특징: U-mode에서 프로그램은 자신의 메모리 공간에만 접근할 수 있으며, 제한된 명령어 세트만 사용할 수 있습니다. 하드웨어 자원에 직접 접근하거나, 시스템 설정을 변경하거나, 운영 체제의 도움 없이 I/O 작업을 수행할 수 없습니다.
사용 예: 워드 프로세서, 웹 브라우저, 게임과 같은 일반적인 사용자 응용 프로그램.

슈퍼바이저 모드 (S-mode):

목적: 이 모드는 운영 체제 커널이 하드웨어 자원, 메모리, 시스템 수준 작업을 관리하는 데 사용됩니다. S-mode는 U-mode보다 높은 특권을 가지며, 더 민감한 작업을 수행할 수 있습니다.
특징: S-mode에서는 하드웨어에 직접 접근하고, 메모리 보호를 관리하며, 시스템 전체 설정을 제어할 수 있습니다. 또한 U-mode 응용 프로그램을 관리하며, 메모리 할당, 프로세스 스케줄링, I/O 작업 등을 처리할 수 있습니다.
사용 예: 리눅스와 같은 운영 체제 커널은 사용자 응용 프로그램을 대신해 하드웨어 자원을 제어하고 관리해야 합니다.

머신 모드 (M-mode):

목적: M-mode는 가장 높은 특권 수준이며, 주로 펌웨어나 하드웨어를 직접 초기화하고 관리하는 저수준 시스템 소프트웨어에 사용됩니다.
특징: M-mode는 모든 하드웨어 자원에 완전히 접근할 수 있으며, 시스템 전체를 구성하고 관리할 수 있는 능력을 가지고 있습니다. 시스템 부팅, 인터럽트 처리, 낮은 Privilege Mode로의 진입 등을 담당합니다.
사용 예: 부트로더, 펌웨어, 시스템 관리 소프트웨어.

하이퍼바이저 모드 (H-mode) (옵션, RISC-V 하이퍼바이저 확장에서 정의됨):

목적: H-mode는 가상 머신(VM)을 관리하는 하이퍼바이저에 사용됩니다. 이 모드는 M-mode와 S-mode 사이에 위치하며, 가상화 지원을 제공합니다.
특징: H-mode는 하이퍼바이저가 S-mode의 여러 가상 인스턴스를 관리할 수 있게 하여, 가상 머신을 생성하고 관리할 수 있도록 합니다.
사용 예: KVM이나 Xen과 같은 가상화 소프트웨어는 동일한 하드웨어에서 여러 운영 체제를 실행할 수 있습니다.

Privilege Mode가 함께 작동하는 방법

모드 간 전환: 낮은 Privilege Mode(U-mode)에서 실행되는 소프트웨어는 시스템 호출을 통해 더 높은 Privilege Mode(S-mode)에서 서비스를 요청할 수 있습니다. 예를 들어, 사용자 응용 프로그램이 I/O 작업을 수행해야 할 때, 시스템 호출을 통해 제어가 S-mode의 운영 체제 커널로 전환됩니다.

보안 및 격리: 특권 수준은 서로 다른 소프트웨어 종류를 분리하여 보안을 강화하도록 설계되었습니다. 예를 들어, U-mode의 사용자 응용 프로그램은 S-mode에서 실행되는 운영 체제 커널에 접근하거나 이를 수정할 수 없기 때문에 버그나 악성 소프트웨어가 시스템을 손상시키지 못하게 됩니다.

트랩 처리: 낮은 Privilege Mode에서 제한된 작업을 실행하려고 시도하면 트랩이 발생하고, 더 높은 Privilege Mode(U-mode에서 S-mode로)로 제어가 전환되어 적절한 조치를 취할 수 있습니다.

 

 

RISC-V 아키텍처는 시스템의 세부 속성을 설정할 수 있는 CSR(Control and Status) 레지스터를 제공합니다. 다음과 같은 모든 속성은 CSR 레지스터를 통해 설정합니다:

  • 익셉션 벡터 베이스 주소 설정
  • MMU를 통한 가상 메모리 설정
  • 주요 Privilege Mode에서 트랩되는 방식

CSR(Control and Status) 레지스터는 시스템의 주요 속성을 설정하므로 부팅 과정에서 대부분 설정됩니다.

CSR 레지스터를 소개했으니 CSR 레지스터에 접근할 수 있는 Privilege Mode에 대해 알아봅시다.

Privilege Mode과 CSR 레지스터 


CSR 레지스터의 이름을 보면 접두사로 m 혹은 s가 보입니다. 주요 CSR 레지스터의 목록은 아래와 같습니다: 

  • Machine Mode: mstatus, mcause, mtvec, mip, mie, matp, mepc
  • Supervisor Mode: sstatus, scause, stvec, sip, sie, satp, sepc

예를 들어 mstatus 레지스터는 Machine Mode에서만 읽거나 쓰고, sstatus 레지스터는 Supervisor Mode에서만 읽거나 쓸 수 있습니다. mstatus와 sstatus 레지스터는 Machine Mode나 Supervisor Mode에서 접근할 수 있을 뿐, mstatus와 sstatus 레지스터를 구성하는 비트 플래그는 같습니다. 

어셈블리 명령어에서 CSR 레지스터를 보면, CSR 레지스터 이름의 접두사에 Privilege Mode를 나타내는 m 혹은 s를 확인할 수 있습니다.

 

관련 강의:

 

RISC-V 아키텍처에서 레지스터를 배우려면 어떤 내용을 알아야 할까요? RISC-V 아키텍처에서는 다음과 같은 레지스터를 제공합니다.

  • 범용 레지스터
  • CSRs(Control and Status) 레지스터

범용 레지스터

먼저 범용 레지스터에 대해 알아 봅시다. 범용 레지스터는 어셈블리 명령어의 입력과 출력을 저장하는 용도로 사용됩니다. 개념 상 C 언어의 변수와 비슷한 개념입니다. 어셈블리 명령어를 분석하실 때 반드시 레지스터를 알아야 합니다.

RISC-V 아키텍처에서 정의된 범용 레지스터의 목록은 다음과 같습니다. 

 

Armv8 아키텍처와 비슷하게 레지스터의 갯수는 X0에서 X31까지 있습니다. 레지스터의 갯수가 많은 편입니다. 대부분 어셈블리 명령어의 인풋과 출력 결과를 저장하기 위해 사용되는데, 함수가 호출될 때 특별히 사용되는 레지스터가 있습니다. 예를 들면:

  • X1(ra): 링크 레지스터, jal 명령어를 실행하면 돌아올 주소를 Hart(RISC-V의 CPU core)가 업데이트
  • X2(sp): 스택 포인터, 프로세스가 가장 마지막에 실행된 스택 주소를 가리킴
  • X10~X17(a0~a7): 함수에 전달되는 아규먼트


리눅스 커널이나 RTOS에서 컨텍스트 스위칭라는 용어를 아마 들으신 적이 있을 것입니다. 여기서 말하는 컨텍스트는 실행 흐름을 뜻하며, 컨텍스트 스위칭은 실행 흐름을 바꾼다라고 볼 수 있습니다. 구체적으로는 컨텍스는 범용 레지스터를 뜻하며, 컨텍스트 스위칭은 범용 레지스터를 바꾼다라고 볼 수 있습니다.

다음은 리눅스 커널에서 컨텍스트를 저장하는 자료구조입니다.  

https://elixir.bootlin.com/linux/v6.9/source/include/linux/sched.h#L748
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
	/*
	 * For reasons of header soup (see current_thread_info()), this
	 * must be the first element of task_struct.
	 */
	struct thread_info		thread_info;
#endif
	unsigned int			__state;
...
	/* CPU-specific state of this task: */
	struct thread_struct		thread;  //<<--

 

task_struct 구조체의 가장 마지막 필드는 thead입니다. 

https://elixir.bootlin.com/linux/v6.9/source/arch/riscv/include/asm/processor.h#L113
/* CPU-specific state of a task */
struct thread_struct {
	/* Callee-saved registers */
	unsigned long ra;
	unsigned long sp;	/* Kernel mode stack */
	unsigned long s[12];	/* s[0]: frame pointer */
	struct __riscv_d_ext_state fstate;
	unsigned long bad_cause;
	u32 riscv_v_flags;
	u32 vstate_ctrl;
	struct __riscv_v_ext_state vstate;
	unsigned long align_ctl;
	struct __riscv_v_ext_state kernel_vstate;
};


보시는 바와 같이 thread 필드에 레지스터를 나타내는 필드가 확인됩니다. 이처럼 범용 레지스터는 운영체제 커널이나 RTOS에서 컨텍스트로 표현됩니다.

 

< 관련 강의 >

 

Privilege Mode를 이해하려면 Privilege level(PL)의 개념을 먼저 알아야 합니다. Privilege Mode는 PL의 기반 위에서 정의했기 때문입니다. 

Privilege level(PL)

사실 대부분 CPU 아키텍처에서 Privilege level(PL)를 정의합니다. Arm 아키텍처를 비롯한 인텔의 x86 아키텍처에서도 Privilege level이란 용어를 볼 수 있습니다. 그렇다면 Privilege level의 실체는 무엇일까요? 그 실체는 3가지로 분류할 수 있습니다:

 

  • 메모리 공간 접근 권한
  • 레지스터 접근 권한
  • 명령어 접근 권한

위에서 언급된 메모리 공간, 레지스터 및 명령어를 접근할 수 있는 권한 레벨을 부여할 수 있는데, 이것이 바로 Privilege level입니다. Privilege level에 대한 예시를 들겠습니다. Privilege level이 가장 높으면 아래와 같은 상황을 그릴 수 있네요.

 

  • 모든 메모리 공간에 접근할 수 있다. 
  • 모든 레지스터에 접근할 수 있다. 
  • 모든 명령어를 실행할 수 있다. 

 

Privilege level이 낮으면 낮을 수록 아래와 같은 조건으로 소프트웨어가 실행됩니다.

 

  • 메모리 공간을 접근하는데 제약이 있다.  
  • 특정 모든 레지스터에 접근할 수 없다. 
  • 특정 명령어를 실행할 수 없다. 

RISC-V 아키텍처에서도 Privilege level(PL)을 정의합니다. 가장 높은 privilege level은 PL3이고 가장 낮은 privilege level은 PL0로 정의합니다.

Privilege level(PL)의 개념을 알아봤으니 이제 Privilege 모드에 대해서 알아봐야 겠네요.

 

RISC-V 프로세서와 관련된 세미나를 진행할 때 입문자들은 종종 다음과 같은 질문을 합니다.

 

RISC-V 아키텍처에서 가장 중요한 기능이 무엇인가요?

 


이 질문에 저는 "RISC-V 아키텍처에서 정의된 권한 모드(Privilege Mode)가 가장 중요하다"라고 답합니다. 그 이유는 무엇일까요? 다음과 같이 요약할 수 있습니다.

  • 익셉션이 유발되면 권한 모드가 바뀌므로 익셉션의 동작 원리를 이해하려면 권한 모드를 알아야 한다.
  • CSR(Control Status Regiser) 레지스터를 제대로 이해하려면 권한 모드를 알아야 한다.
  • 어셈블리 명령어의 동작 원리를 파악하려면 권한 모드를 알아야 한다.

익셉션과 레지스터를 비롯한 RISC-V 아키텍처의 많은 기능은 권한 모드 기반 위에서 설계돼 있어 권한 모드를 이해하는 것이 중요합니다.

 

이 의견을 듣고 입문자들이 권한 모드를 공부하기 시작합니다. 그런데 기계적으로 스펙의 내용을 제대로 이해하지 못한 채 억지로 암기하는 모습을 많이 봤습니다. RISC-V에서 정의된 권한 모드를 효과적으로 배우는 방법은 무엇일까요? 다음과 같은 질문을 던지고 답을 찾는 과정을 거치면 공부한 내용이 머릿속에 더 오랫동안 남을 것입니다.

  • 권한 모드는 어떻게 바뀔까?
  • 권한 모드가 바뀌면 이전 권한 모드는 어떻게 확인할까?
  • 이전 권한 모드로는 어떻게 복귀할까?
  • 권한 모드를 활용해 운영체제 커널은 어떻게 구현돼 있을까?

 

위와 같은 질문을 스스로 던지면서 프로그래밍하고 관련 코드를 분석하면 여러분이 작성한 코드가 어떤 권한 모드로 실행되는지 더 잘 이해할 수 있을 것입니다.

 



앞으로 업데이트할 포스트에서는 RISC-V 아키텍처의 권한 모드를 소개하고 권한 모드와 관련된 레지스터를 소개합니다.

RISC-V IPI Call: Code walkthrough

void smp_call_function_many(const struct cpumask *mask,
			    smp_call_func_t func, void *info, bool wait)
{
	smp_call_function_many_cond(mask, func, info, wait * SCF_WAIT, NULL);
}
EXPORT_SYMBOL(smp_call_function_many);

void send_call_function_single_ipi(int cpu)
{
	struct rq *rq = cpu_rq(cpu);

	if (!set_nr_if_polling(rq->idle))
		arch_send_call_function_single_ipi(cpu);
	else
		trace_sched_wake_idle_without_ipi(cpu);
}

arch/riscv/kernel/smp.c
void arch_send_call_function_single_ipi(int cpu)
{
	send_ipi_single(cpu, IPI_CALL_FUNC);
}

static void send_ipi_single(int cpu, enum ipi_message_type op)
{
	smp_mb__before_atomic();
	set_bit(op, &ipi_data[cpu].bits);
	smp_mb__after_atomic();

	if (ipi_ops && ipi_ops->ipi_inject)
		ipi_ops->ipi_inject(cpumask_of(cpu));
	else
		pr_warn("SMP: IPI inject method not available\n");
}

static const struct riscv_ipi_ops sbi_ipi_ops = {
	.ipi_inject = sbi_send_cpumask_ipi
};


static void sbi_send_cpumask_ipi(const struct cpumask *target)
{
	sbi_send_ipi(target);
}

int sbi_send_ipi(const struct cpumask *cpu_mask)
{
	return __sbi_send_ipi(cpu_mask);
}

arch/riscv/kernel/sbi.c
void __init sbi_init(void)
{
...
	if (!sbi_spec_is_0_1()) {
		pr_info("SBI implementation ID=0x%lx Version=0x%lx\n",
			sbi_get_firmware_id(), sbi_get_firmware_version());
		if (sbi_probe_extension(SBI_EXT_TIME)) {
			__sbi_set_timer = __sbi_set_timer_v02;
			pr_info("SBI TIME extension detected\n");
		} else {
			__sbi_set_timer = __sbi_set_timer_v01;
		}
		if (sbi_probe_extension(SBI_EXT_IPI)) {
			__sbi_send_ipi	= __sbi_send_ipi_v02;
			pr_info("SBI IPI extension detected\n");
		} else {
			__sbi_send_ipi	= __sbi_send_ipi_v01;
		}


arch/riscv/kernel/sbi.c
static int __sbi_send_ipi_v01(const struct cpumask *cpu_mask)
{
	pr_warn("IPI extension is not available in SBI v%lu.%lu\n",
		sbi_major_version(), sbi_minor_version());

	return 0;
}

static int __sbi_send_ipi_v02(const struct cpumask *cpu_mask)
{
...
	if (hmask) {
		ret = sbi_ecall(SBI_EXT_IPI, SBI_EXT_IPI_SEND_IPI,
				hmask, hbase, 0, 0, 0, 0);
		if (ret.error)
			goto ecall_failed;
	}

	return 0;

ecall_failed:
	result = sbi_err_map_linux_errno(ret.error);
	pr_err("%s: hbase = [%lu] hmask = [0x%lx] failed (error [%d])\n",
	       __func__, hbase, hmask, result);
	return result;
}


struct sbiret sbi_ecall(int ext, int fid, unsigned long arg0,
			unsigned long arg1, unsigned long arg2,
			unsigned long arg3, unsigned long arg4,
			unsigned long arg5)
{
	struct sbiret ret;

	register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0);
	register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1);
	register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2);
	register uintptr_t a3 asm ("a3") = (uintptr_t)(arg3);
	register uintptr_t a4 asm ("a4") = (uintptr_t)(arg4);
	register uintptr_t a5 asm ("a5") = (uintptr_t)(arg5);
	register uintptr_t a6 asm ("a6") = (uintptr_t)(fid);
	register uintptr_t a7 asm ("a7") = (uintptr_t)(ext);
	asm volatile ("ecall"
		      : "+r" (a0), "+r" (a1)
		      : "r" (a2), "r" (a3), "r" (a4), "r" (a5), "r" (a6), "r" (a7)
		      : "memory");
	ret.error = a0;
	ret.value = a1;

	return ret;
}
EXPORT_SYMBOL(sbi_ecall);

ffffffff80006a20 <sbi_ecall>:
ffffffff80006a20:	1101                	add	sp,sp,-32
ffffffff80006a22:	ec22                	sd	s0,24(sp)
ffffffff80006a24:	832a                	mv	t1,a0
ffffffff80006a26:	1000                	add	s0,sp,32
ffffffff80006a28:	8e2e                	mv	t3,a1
ffffffff80006a2a:	8532                	mv	a0,a2
ffffffff80006a2c:	85b6                	mv	a1,a3
ffffffff80006a2e:	863a                	mv	a2,a4
ffffffff80006a30:	86be                	mv	a3,a5
ffffffff80006a32:	8742                	mv	a4,a6
ffffffff80006a34:	87c6                	mv	a5,a7
ffffffff80006a36:	8872                	mv	a6,t3
ffffffff80006a38:	889a                	mv	a7,t1
ffffffff80006a3a:	00000073          	ecall
ffffffff80006a3e:	6462                	ld	s0,24(sp)
ffffffff80006a40:	6105                	add	sp,sp,32
ffffffff80006a42:	8082                	ret

 

 

AMD:

https://docs.amd.com/r/en-US/ug1629-microblaze-v-user-guide/Machine-Cause-Register-mcause

 

SBI specification:

https://www.scs.stanford.edu/~zyedidia/docs/riscv/riscv-sbi.pdf

SBI Reference:

https://docs.rs/sbi/latest/sbi/legacy/index.html

The RISC-V Advanced Interrupt Architecture:

https://github.com/riscv/riscv-aia/releases

 

윈도우 

WSL 2 환경에서 아래 툴들을 설치하자. virt-edit 에서 Fedora 이미지의 /boot/extlinux/extlinux.conf 를 수정하기 위해 필요하다.

 

 

더 자세한 자료는 아래 링크를 참고하자.

https://fedoraproject.org/wiki/Architectures/RISC-V/Installing#Boot_under_QEMU



Below link is pretty good.

 

https://www.cs.cornell.edu/courses/cs3410/2019sp/riscv/interpreter/#

 

RISC-V Interpreter

Credit to Danny Qiu for the creation of the original MIPS interpreter.

www.cs.cornell.edu

 

 

Code walkthrough 

 

MP:FFFFFFFF800030B8|ret_from_exception:     c.ldsp     x8,0x100(x2)   ; x8,256(x2)
MP:FFFFFFFF800030BA|                        csrci      sstatus,0x2   ; sstatus,2
MP:FFFFFFFF800030BE|                        andi       x8,x8,0x100   ; x8,x8,256
MP:FFFFFFFF800030C2|                        c.bnez     x8,0xFFFFFFFF800030D8   ; x8,restore_all
MP:FFFFFFFF800030C4|resume_userspace:       ld         x8,0x0(x4)    ; x8,0(x4)
MP:FFFFFFFF800030C8|                        andi       x9,x8,0x60E   ; x9,x8,1550
MP:FFFFFFFF800030CC|                        c.bnez     x9,0xFFFFFFFF8000312A   ; x9,work_pending
MP:FFFFFFFF800030CE|                        c.addi4spn x8,0x120      ; x8,288
MP:FFFFFFFF800030D0|                        sd         x8,0x10(x4)   ; x8,16(x4)
MP:FFFFFFFF800030D4|                        csrw       sscratch,x4
MP:FFFFFFFF800030D8|restore_all:            c.ldsp     x10,0x100(x2)   ; x10,256(x2)
MP:FFFFFFFF800030DA|                        c.ldsp     x12,0x0(x2)   ; x12,0(x2)
MP:FFFFFFFF800030DC|                        sc.d       x0,x12,(x2)
MP:FFFFFFFF800030E0|                        csrw       sstatus,x10
MP:FFFFFFFF800030E4|                        csrw       sepc,x12
MP:FFFFFFFF800030E8|                        c.ldsp     x1,0x8(x2)    ; x1,8(x2)
MP:FFFFFFFF800030EA|                        c.ldsp     x3,0x18(x2)   ; x3,24(x2)
MP:FFFFFFFF800030EC|                        c.ldsp     x4,0x20(x2)   ; x4,32(x2)
MP:FFFFFFFF800030EE|                        c.ldsp     x5,0x28(x2)   ; x5,40(x2)
MP:FFFFFFFF800030F0|                        c.ldsp     x6,0x30(x2)   ; x6,48(x2)
MP:FFFFFFFF800030F2|                        c.ldsp     x7,0x38(x2)   ; x7,56(x2)
MP:FFFFFFFF800030F4|                        c.ldsp     x8,0x40(x2)   ; x8,64(x2)
...
MP:FFFFFFFF80003122|                        c.ldsp     x31,0xF8(x2)   ; x31,248(x2)
MP:FFFFFFFF80003124|                        c.ldsp     x2,0x10(x2)   ; x2,16(x2)
MP:FFFFFFFF80003126|                        sret

+ Recent posts