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에서 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로)로 제어가 전환되어 적절한 조치를 취할 수 있습니다.

 

 

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 아키텍처의 권한 모드를 소개하고 권한 모드와 관련된 레지스터를 소개합니다.

+ Recent posts