Skip to content

[RISC‐V 64] 2. mcount‐arch.h 구현 과정

JMKim edited this page Aug 29, 2023 · 2 revisions

1. RISC-V 64bit 아키텍처의 mcount-arch.h 구현

1. RISC-V 64bit의 mcount-arch.h 구현을 위한 예상 작업 목록

1-1. TO-DO List

  1. aarch64, arm, i386, x86_64 아키텍처들의 레지스터 분석

  2. aarch64, arm, i386, x86_64 아키텍처들의 mcount-arch.h 파일들 분석

  3. RISC-V 64bit struct mcount_regs 구조체 구현

  4. RISC-V 64bit 레지스터의 Argument 값 접근을 위한 ARG 1 ~ N개의 #define 구현

  5. ARCH_MAX_REG_ARGS, ARCH_MAX_FLOAT_ARGS, ARCH_NUM_BASE_REGS 중 필요한 #define 구현

  6. RISC-V 64bit struct mcount_arch_context 구조체 구현

  • 단, arm 아키텍처와 같이 해당 구조체는 선언만 해두고 구현하지 않는 경우도 있는 것으로 보아 RISC-V 64bit도 이에 해당하는지는 RISC-V 아키텍처 분석 필요
  1. struct uftrace_sym_info 구조체는 utils/symbol.h에 존재하기 때문에 구현하지 않아도 동작에 영향이 없을 것으로 보임

  2. 각 아키텍처 별 mcount-arch.h를 참조하여 나머지 기능 구현 및 테스트

1-2. 제한사항

  • 여기서 구현할 mcount-arch.h는 빌드 시 mcount 를 사용하도록 컴파일 해 추적하는 기능만 지원하며, 아래와 같은 이유로 Dynamic Tracing은 지원하지 않는다.
    • Dynamic Tracing 기능까지 구현하는데 필요한 시간 부족, Dynamic Tracing이 동작하는 내부 구조에 대한 이해 부족

1. RISC-V 64bit의 mcount-arch.h 구현을 위한 예상 작업 목록

1-1. TO-DO List

  1. aarch64, arm, i386, x86_64 아키텍처들의 레지스터 분석

  2. aarch64, arm, i386, x86_64 아키텍처들의 mcount-arch.h 파일들 분석

  3. RISC-V 64bit struct mcount_regs 구조체 구현

  4. RISC-V 64bit 레지스터의 Argument 값 접근을 위한 ARG 1 ~ N개의 #define 구현

  5. ARCH_MAX_REG_ARGS, ARCH_MAX_FLOAT_ARGS, ARCH_NUM_BASE_REGS 중 필요한 #define 구현

  6. RISC-V 64bit struct mcount_arch_context 구조체 구현

  • 단, arm 아키텍처와 같이 해당 구조체는 선언만 해두고 구현하지 않는 경우도 있는 것으로 보아 RISC-V 64bit도 이에 해당하는지는 RISC-V 아키텍처 분석 필요
  1. struct uftrace_sym_info 구조체는 utils/symbol.h에 존재하기 때문에 구현하지 않아도 동작에 영향이 없을 것으로 보임

  2. 각 아키텍처 별 mcount-arch.h를 참조하여 나머지 기능 구현 및 테스트

1-2. 제한사항

  • 여기서 구현할 mcount-arch.h는 빌드 시 mcount 를 사용하도록 컴파일 해 추적하는 기능만 지원하며, 아래와 같은 이유로 Dynamic Tracing은 지원하지 않는다.
    • Dynamic Tracing 기능까지 구현하는데 필요한 시간 부족, Dynamic Tracing이 동작하는 내부 구조에 대한 이해 부족

2. 각 아키텍처 별 레지스터 분석

  • mcount-arch.h 에서 정의되는 struct mcount_regs 구조체와 struct mcount_arch_context 구조체를 이해하기 위해서는 각 아키텍처 별 레지스터 목록에 대한 이해가 필요하다.

  • **struct mcount_regs는** 각 아키텍처의 정수 레지스터 목록 중 Argument와 관련된 레지스터의 값을 저장하기 위한 용도이고, struct mcount_arch_context 각 아키텍처의 부동 소수점 레지스터 목록 중 Argument와 관련된 레지스터의 값을 저장하기 위한 용도로 사용된다.

2-1. x86_64 아키텍처의 레지스터 목록

2-1-1. x86_64 아키텍처의 정수 레지스터 목록

2-1-2. x86_64 아키텍처의 부동 소수점 레지스터 목록

  • x86_64의 부동 소수점 레지스터는 아래 그림과 같이 MMX 레지스터와 XMM 레지스터로 구성된다.

  • 그럼 x86_64 아키텍처에는 왜 2개의 부동 소수점 레지스터인 MMX와 XMM 레지스터가 존재하고, uftrace의 아키텍처 코드에서는 XMM 레지스터를 struct mcount_arch_context의 필드로 사용하는지는 아래 내용에 해당한다.

    • 먼저 부동 소수점 레지스터는 SIMD(Single Instruction Multiple Data)라는 하나의 명령어로 여러개의 데이터를 처리할 수 있는 명령어 셋이 도입될 때 추가되어 왔음

    • SIMD 명령어 셋은 MMX → SSE → SSE2 → SSE3 → SSSE3 → SSE4(SSE4.1→ SSE4.2→ SSE4a) → AVX → AVX2 → AVX-512 순서로 발전됨

    • MMX 레지스터는 인텔이 1997년에 MMX(MultiMedia eXtension) 명령어 집합을 추가하며 도입된 레지스터 목록이고, XMM 레지스터는 인텔이 1999년에 SSE(Stream SIMD Extensions) 명령어 집합을 추가하며 도입된 레지스터 목록

    • 그 이후 추가된 AVX에서는 XMM 레지스터에 YMM, ZMM 레지스터를 추가하여 확장하는 개념으로 사용하고 있다.

    • 결론적으로, 이미 만들어져 MMX 명령어 셋을 사용하는 레거시 코드가 존재하겠지만, 최근에는 주로 SSE 이상부터 사용되기 때문에 XMM 레지스터를 struct mcount_arch_context의 필드로 사용하는 것으로 보인다.

2-2. aarch64 아키텍처의 레지스터 목록

2-2-1. aarch64 아키텍처의 정수 레지스터 목록

2-2-2. aarch64 아키텍처의 부동 소수점 레지스터 목록

3. RISC-V 아키텍처에 대한 개요

  • RISC-V 아키텍처에는 RV32I, RV32E RV64I, RV128I 라고 하는 4가지의 ISA(Instruction Set Architecture)와 Extension이라는 개념이 존재하는데 추후 RISC-V 포팅을 위한 참고 자료나 문서를 볼 때 어떤 부분이 필요하고 필요하지 않은지 구분하기 위해서는 해당 내용이 필요하다고 판단해 해당 내용을 작성한다.

3-1. RISC-V ISA(Instruction Set Architecture)

  • RISC-V는 오픈소스 아키텍처이기 때문에 인텔, AMD, ARM과 같이 아키텍처를 공개하지 않는 CPU 분야에서 리눅스 커널과 같은 존재
  • 하드웨어 칩을 설계하거나 제작할 능력을 가진 사람들은 RISC-V 아키텍처를 사용해 원하는 CPU를 만들 수 있다.
  • 그렇다보니 기본적인 명령어들을 탑재한 ISA에 이후 서술할 Extension 들을 결합해 원하는 기능을 하드웨어 레벨에서 수행하는 CPU를 만들 수 있으며 아키텍처 명명 규칙이 조금 복잡하다는 점이 특징
    • 보통은 RISC-V CPU라고 하겠지만, 아키텍처 명을 조금 더 세부적으로 들여다보게 되면 RV64GC 와 같은 문자열을 사용하는 것을 볼 수 있다.
    • RV는 RISC-V의 아키텍처를 사용하는 경우 항상 붙는 이름이고, RV 다음에 붙은 숫자는 명령어가 처리하는 bit 수 를 의미하며, 그 다음에 오는 문자들은 RISC-V CPU를 만들 때 추가한 확장의 약어를 붙이게 된다.

3-2. RISC-V Extension

  • RISC-V의 Extension은 위에서 잠깐 서술한 것과 같이 선택한 ISA에 미리 정의된 기능(=Extension)들 중 원하는 기능들을 블록을 결합하듯 추가하는 개념이라고 이해하면 된다.

  • 저전력 임베디드용 RISC-V 아키텍처인 RV32E를 제외하고는 모두 I를 공통적으로 가지기 때문에 생략이 가능하고, 그 외에는 Extension의 약어를 붙이게 되어있다.

    • 다만, 최근에는 사용되는 확장이 많아짐에 따라 자주 사용되는 확장들을 그룹화 한 “G”라는 확장을 정의해 사용하기도 한다.
    • 자세한 확장 목록 및 설명은 아래 링크 참조
  • 멘토님이 작성하신 문서에서는 RISC-V Ubuntu 23.04를 QEMU에서 실행하기 위해서는 -cpu 옵션에 sifive-u54를 줘야 한다고 되어있기 때문에, sifive-u54 CPU의 아키텍처 명명 규칙을 확인하기 위해 참조한 구조도는 아래 그림과 같다.

    image

3-3. RV32I, RV64I, RV128I 간 레지스터 차이점

  • 레지스터 부분에 한해서 아래 그림에서 볼 수 있는 것과 같이 RV32I, RV64I, RV128I 간 차이점은 레지스터의 크기 차이이며, 레지스터의 수와 역할은 동일한 것으로 보인다.

  • 추후 uftrace가 RISC-V 32bit 를 지원할 수 있도록 포팅하는 작업을 진행하더라도 RISC-V 64bit의 파일들의 내용을 일부 가져다 사용할 수 있을 것으로 보여 포팅 작업이 조금 더 수월할 것으로 보여진다.

4. RISC-V 아키텍처의 레지스터 분석

4-1. RISC-V 아키텍처 문서

  • RISC-V의 아키텍처 문서는 “Volume 1, Unprivileged Specification” 과 “Volume 2, Privileged Specification”으로 구성되며, 레지스터 목록과 관련된 내용은 “Volume 1, Unprivileged Specification”에 존재한다.
  • 아래 링크에 접속해 “Volume 1, Unprivileged Specification” PDF 파일을 다운로드 받아야 한다.

4-2. RISC-V 아키텍처 레지스터 목록

4-2-1. RISC-V 아키텍처의 정수 및 부동 소수점 레지스터 목록

  • RISC-V 아키텍처는 아래 그림의 표와 같이 x10 ~ x17에 해당하는 총 8개의 레지스터를 Argument 목적으로 사용하고 있다.

    image

5. RISC-V 아키텍처의 mcount-arch.h 구현

5-1. struct mcount_regs 구조체 구현

  • objdump -d 명령을 사용하여 디스어셈블 시 register의 이름이 아닌 ABI Name에 해당하는 레지스터 이름이 출력되기 때문에 아래와 같이 구현한다.

    struct mcount_regs {
    	unsigned long a0;
    	unsigned long a1;
    	unsigned long a2;
    	unsigned long a3;
    	unsigned long a4;
    	unsigned long a5;
    	unsigned long a6;
    	unsigned long a7;
    };

5-2. 레지스터 값 접근을 위한 매크로 선언

  • ARG1 ~ ARG8은 Argument 목적으로 사용되는 레지스터의 값을 얻어올 때 사용되는 매크로 이며, struct mcount_regs 구조체에 구현된 필드들의 수 만큼 선언한다.

  • ARCH_MAX_REG_ARGS는 Argument 목적으로 사용되는 레지스터의 최대 갯수를 정의

    #define ARG1(a) ((a)->a0)
    #define ARG2(a) ((a)->a1)
    #define ARG3(a) ((a)->a2)
    #define ARG4(a) ((a)->a3)
    #define ARG5(a) ((a)->a4)
    #define ARG6(a) ((a)->a5)
    #define ARG7(a) ((a)->a6)
    #define ARG8(a) ((a)->a7)
    
    #define ARCH_MAX_REG_ARGS 8

5-3. struct mcount_arch_context 구조체 구현

5-3-1. struct mcount_arch_context 구조체가 나오게 된 이유 분석

  • arm 아키텍처와 같이 struct mcount_arch_context 구조체가 구현되지 않은 경우도 있기 때문에 RISC-V에서도 해당 구조체가 구현이 되어야 하는지 의문을 해결하기 위해서는 다른 아키텍처가 해당 구조체를 구현한 이유를 알아야 한다.

  • 모든 아키텍처 공통 이슈

    • 아래 커밋 링크에서 x86_64는 필드까지 구현하였지만, 나머지 아키텍처는 필드를 비워 둔 상태로 struct mcount_arch_context를 구현하였다. (다만, 이 때에는 i386은 지원하지 않아 arch 폴더에 i386은 존재하지 않음)
      • 이후 이력을 보면 i386을 지원할 때 처음부터 struct mcount_arch_context를 구현하였다.
    • 해당 커밋은 uftrace record 시 Python이나 Luajit 스크립트가 활성화 되면 부동 소수점 레지스터와 같은 일부 아키텍처 정보가 변경되기 때문에 발생하는 이슈를 해결하는 커밋으로 되어있다.
  • aarch64 아키텍처 이슈

    • 아래 PR 링크에 따르면, aarch64에서 부동소수점을 사용하는 함수 인자나 반환 값이 잘못되는 문제가 있었고 이를 해결하는 코드 중 일부가 아래에 해당한다고 한다.
  • 아직 이해가 되지 않는 부분 (=이슈로 만들어서 질문이 필요한 부분)

    • arm 아키텍처에서는 struct mcount_arch_context 구조체를 구현하지 않는 이유

    • aarch64 아키텍처는 ARCH_MAX_FLOAT_REGS가 8인데, arm 아키텍처의 경우 ARCH_MAX_FLOAT_REGS가 16인 이유

      /* arch/arm/mount-arch.h */
      ......
      
      #define ARCH_MAX_FLOAT_REGS 16
      #define ARCH_MAX_DOUBLE_REGS 8
      
      struct mcount_arch_context {};
      
      ......
      /* arch/aarch64/mount-arch.h */
      ......
      
      #define ARCH_MAX_FLOAT_REGS 8
      
      #define HAVE_MCOUNT_ARCH_CONTEXT
      struct mcount_arch_context {
      	double d[ARCH_MAX_FLOAT_REGS];
      };
      
      ......
    • 해당 부분은 arm 아키텍처의 부동 소수점 레지스터 분석이 필요할 것으로 보임

5-3-2. RISC-V 64bit의 struct mcount_arch_context 구조체 구현

  • uftrace는 함수의 인자나 반환 값을 출력하기 때문에 의도하지 않은 결과에 의해 함수의 인자나 반환 값이 덮어씌워지거나 사라지면 안되기 때문에 RISC-V 아키텍처의 함수 호출 인자에 사용되는 부동 소수점 레지스터인 f10 ~ f17의 값을 저장하고 복구해야 한다고 판단하여 아래와 같이 구현한다.

    #define ARCH_MAX_FLOAT_REGS 8
    
    #define HAVE_MCOUNT_ARCH_CONTEXT
    struct mcount_arch_context {
    	double f[ARCH_MAX_FLOAT_REGS];
    };
  • 추후 아래 PR의 테스트를 위한 예제 코드를 돌려 struct mcount_arch_context 구조체가 정상적으로 동작하는지 확인이 필요하다.

    1. https://github.com/namhyung/uftrace/pull/1122

5-3-3. RISC-V 64bit의 NOP 명령어 크기 분석

  • NOP_INSN_SIZE는 NOP 어셈블리 명령어의 크기를 지정하는 것으로, 명령어의 크기와 필드 정보들은 4-1. RISC-V 아키텍처 문서에서 다운로드 받은 ““Volume 1, Unprivileged Specification” 에서 확인할 수 있다.

  • 단, 여기서 조심해야 할 부분은 RISC-V 어셈블리 명령어의 약 50% ~60%를 압축된 명령어로 대체시켜 사용하는 “C” Extension이 사용되었을 경우이다.

    • 예전에 RV64GC에 해당하는 RISC-V CPU의 명령어를 분석하는데 이 부분을 몰라서 어셈블리 명령어를 RV64I 기준으로 분석하다가 맞지 않아 고민했던 적이 있었음

    • “C” Extension이 사용되는 RISC-V CPU의 NOP 명령어는 아래와 같이 구성되고, C.NOP라는 명칭으로 구분된다. (이때 Compressed NOP Instruction의 길이는16bit기 때문에 2Byte에 해당)

      image

  • “C” Extension 이 사용되지 않는 경우 RV32I, RV64I, RV128I 모두 RV32I의 NOP 명령어 구조를 사용하기 때문에 아래와 같이 구성된다.

    • “C” Extension이 사용되지 않는 경우 RISC-V CPU의 NOP 명령어는 아래와 같이 구성되고, NOP 명칭 그대로 사용된다.(이때 NOP Instruction의 길이는 32bit기 때문에 4Byte에 해당)

      image

  • 결론적으로, “C” Extension이 사용될 때와 사용되지 않을 때 NOP 명령어의 크기에 차이가 있어 “C” Extension의 유무를 확인할 수 있는 방법이 필요하다.

5-3-4. RISC-V 아키텍처의 Extension 확인하기

  • 현재 시스템에서 돌아가고 있는 RISC-V CPU의 아키텍처를 확인할 수 있는 방법은 Qemu 에서 돌아가는 RISC-V Ubuntu를 기준으로 아래와 같이 확인할 수 있었다.
    • cat /proc/cpuinfo 명령을 실행한 결과로, isa라는 필드에서 extension 목록들이 표현되고 있었다.

    • qemu가 아닌 실제 보드에서도 동일한 결과를 얻을 수 있는지 확인이 필요하다.

      image

5-3-5. RISC-V 64bit의 NOP_INSN_SIZE 정의

  • 현재 Qemu에서 RISC-V 64bit 가상 환경을 사용하기 위한 Preinstalled Image는 원래 SiFive HiFive Unmatched 보드를 위한 이미지이지만, Qemu를 지원하는 유일한 이미지이기도 하다.

  • Ubuntu 22.04의 경우에도 Ubuntu 23.04와 동일한 보드를 지원하는 이미지이기 때문에 Sifive U54에서 동작하도록 구현되었을 것이며, 3-2. RISC-V Extension에서 설명한 내용대로 Sifive U54는 RV64GC에 해당하여 “C” Extension이 적용된 상태이다.

  • 따라서, 해당 uftrace 포팅용 코드는 “C” Extension이 적용된 시스템에서만 돌아갈 수 있도록 임시적으로 NOP_INSN_SIZE를 아래와 같이 정의하였다.

    • 다만, 위에서 서술한 extension을 확인할 수 있는 방법이 맞다면, /proc/cpuinfo 파일의 내용을 읽어 “C” Extension 여부에 따라 NOP_INSN_SIZE를 다르게 설정할 수 있어야 한다.
    #define NOP_INSN_SIZE 2

5-4. RISC-V 64bit의 mcount-arch.h 최종 코드

  • Dynamic Tracing을 지원하지 않는 RISC-V 64bit의 mcount-arch.h 코드는 아래와 같다.

    • Dynamic Tracing을 지원하지 않는 이유는 여기 확인
    #ifndef MCOUNT_ARCH_H
    #define MCOUNT_ARCH_H
    
    #define mcount_regs mcount_regs
    
    struct mcount_regs {
    	unsigned long a0;
    	unsigned long a1;
    	unsigned long a2;
    	unsigned long a3;
    	unsigned long a4;
    	unsigned long a5;
    	unsigned long a6;
    	unsigned long a7;
    };
    
    #define ARG1(a) ((a)->a0)
    #define ARG2(a) ((a)->a1)
    #define ARG3(a) ((a)->a2)
    #define ARG4(a) ((a)->a3)
    #define ARG5(a) ((a)->a4)
    #define ARG6(a) ((a)->a5)
    #define ARG7(a) ((a)->a6)
    #define ARG8(a) ((a)->a7)
    
    #define ARCH_MAX_REG_ARGS 8
    #define ARCH_MAX_FLOAT_REGS 8
    
    #define HAVE_MCOUNT_ARCH_CONTEXT
    struct mcount_arch_context {
    	double f[ARCH_MAX_FLOAT_REGS];
    };
    
    #define NOP_INSN_SIZE 2
    
    #endif /* MCOUNT_ARCH_H */

6. RISC-V CPU에 적용된 Extension 확인

  • 5-3-5. RISC-V 64bit의 NOP_INSN_SIZE 정의에서 설명한 것과 같이 “C” Extension의 유무에 따라 NOP Instruction의 크기가 달라지기 때문에 확인할 수 있는 방법이 필요했고, /proc/cpuinfo 파일에서 해당 정보를 확인할 수 있었다.

  • 하지만, NOP_INSN_SIZE를 정의하는 부분을 런타임 시가 아닌 컴파일 시점에 적용하고 싶었고 misc/install-elfutils.sh 파일의 9번 째 줄에서 힌트를 얻을 수 있었다.

    9 : n_cpus=$(grep -c ^processor /proc/cpuinfo)
  • 다만, 위의 명령은 processor라는 문자열이 일치하는 행의 수를 반환하는 명령이기 때문에 바로사용할 수 없어 아래와 같은 명령어 조합을 생성했다. (혹시 가능하다면 사용되는 명령어의 수를 줄이고 싶은데 고민이 필요하다.)

    grep ^isa /proc/cpuinfo | uniq | sed 's/ //g' | cut -f 2 -d':'
    • 위 명령을 실행한 결과는 아래와 같고, isa 필드의 값만 추출하는 것을 확인할 수 있다.

      image

  • 이 내용은 추후 arch/riscv64 폴더 내부의 Makefile을 구현할 때 넣어아 하는지 아니면 최상위 디렉토리의 Makefile에 넣어야 하는지는 고민이 필요하다.

  • 해당 내용이 실제 RISC-V 보드에서도 동일하게 적용되는지는 확인이 필요하다.

Clone this wiki locally