-
Notifications
You must be signed in to change notification settings - Fork 0
[RISC‐V 64] 2. mcount‐arch.h 구현 과정
-
aarch64, arm, i386, x86_64 아키텍처들의 레지스터 분석
-
aarch64, arm, i386, x86_64 아키텍처들의 mcount-arch.h 파일들 분석
-
RISC-V 64bit struct mcount_regs 구조체 구현
-
RISC-V 64bit 레지스터의 Argument 값 접근을 위한 ARG 1 ~ N개의 #define 구현
-
ARCH_MAX_REG_ARGS, ARCH_MAX_FLOAT_ARGS, ARCH_NUM_BASE_REGS 중 필요한 #define 구현
-
RISC-V 64bit struct mcount_arch_context 구조체 구현
- 단, arm 아키텍처와 같이 해당 구조체는 선언만 해두고 구현하지 않는 경우도 있는 것으로 보아 RISC-V 64bit도 이에 해당하는지는 RISC-V 아키텍처 분석 필요
-
struct uftrace_sym_info 구조체는 utils/symbol.h에 존재하기 때문에 구현하지 않아도 동작에 영향이 없을 것으로 보임
-
각 아키텍처 별 mcount-arch.h를 참조하여 나머지 기능 구현 및 테스트
- 여기서 구현할 mcount-arch.h는 빌드 시 mcount 를 사용하도록 컴파일 해 추적하는 기능만 지원하며, 아래와 같은 이유로 Dynamic Tracing은 지원하지 않는다.
- Dynamic Tracing 기능까지 구현하는데 필요한 시간 부족, Dynamic Tracing이 동작하는 내부 구조에 대한 이해 부족
-
aarch64, arm, i386, x86_64 아키텍처들의 레지스터 분석
-
aarch64, arm, i386, x86_64 아키텍처들의 mcount-arch.h 파일들 분석
-
RISC-V 64bit struct mcount_regs 구조체 구현
-
RISC-V 64bit 레지스터의 Argument 값 접근을 위한 ARG 1 ~ N개의 #define 구현
-
ARCH_MAX_REG_ARGS, ARCH_MAX_FLOAT_ARGS, ARCH_NUM_BASE_REGS 중 필요한 #define 구현
-
RISC-V 64bit struct mcount_arch_context 구조체 구현
- 단, arm 아키텍처와 같이 해당 구조체는 선언만 해두고 구현하지 않는 경우도 있는 것으로 보아 RISC-V 64bit도 이에 해당하는지는 RISC-V 아키텍처 분석 필요
-
struct uftrace_sym_info 구조체는 utils/symbol.h에 존재하기 때문에 구현하지 않아도 동작에 영향이 없을 것으로 보임
-
각 아키텍처 별 mcount-arch.h를 참조하여 나머지 기능 구현 및 테스트
- 여기서 구현할 mcount-arch.h는 빌드 시 mcount 를 사용하도록 컴파일 해 추적하는 기능만 지원하며, 아래와 같은 이유로 Dynamic Tracing은 지원하지 않는다.
- Dynamic Tracing 기능까지 구현하는데 필요한 시간 부족, Dynamic Tracing이 동작하는 내부 구조에 대한 이해 부족
-
mcount-arch.h
에서 정의되는struct mcount_regs
구조체와struct mcount_arch_context
구조체를 이해하기 위해서는 각 아키텍처 별 레지스터 목록에 대한 이해가 필요하다. -
**struct mcount_regs
는** 각 아키텍처의 정수 레지스터 목록 중 Argument와 관련된 레지스터의 값을 저장하기 위한 용도이고,struct mcount_arch_context
는 각 아키텍처의 부동 소수점 레지스터 목록 중 Argument와 관련된 레지스터의 값을 저장하기 위한 용도로 사용된다.
- arch/x86_64/mcount-arch.h에 정의된 struct mcount_regs 구조체의 필드는 아래 그림에서 6 ~ 1의 순서로 argument 레지스터가 구조체의 필드로 구성된 것을 확인할 수 있다.
-
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 레지스터를 추가하여 확장하는 개념으로 사용하고 있다.
-
그림 원본 링크 : https://commons.wikimedia.org/wiki/File:AVX_registers.svg
AVX-2 명령어셋 레지스터 목록
-
-
결론적으로, 이미 만들어져 MMX 명령어 셋을 사용하는 레거시 코드가 존재하겠지만, 최근에는 주로 SSE 이상부터 사용되기 때문에 XMM 레지스터를 struct mcount_arch_context의 필드로 사용하는 것으로 보인다.
- 참조 링크 : https://stackoverflow.com/questions/44299401/difference-between-mmx-and-xmm-register
- 그럼 AVX-2, AVX-512에서 도입된 YMM, ZMM 레지스터는..? (만약 AVX를 사용하는 프로그램을 uftrace로 추적하게 되었을 때 문제가 생기는지 확인이 필요한 부분)
-
- arch/aarch64/mcount-arch.h에 정의된 struct mcount_regs 구조체의 필드는 아래 그림에서 Argument와 관련된 레지스터인 x0 ~ x7 까지가 필드로 구현된 것임을 확인할 수 있다.
-
arch/aarch64/mcount-arch.h에 정의된 struct mcount_arch_context 구조체의 필드는 아래 그림에서 Argument와 관련된 레지스터인 v0 ~ v7 까지가 필드로 구현된 것임을 확인할 수 있다.
-
부동 소수점 레지스터는 내부적으로 크기에 따라 D(64bit 크기로 접근 시), S(32bit 크기로 접근 시), H(16bit 크기로 접근 시)로 접근이 가능한데 아래 그림과 같이 실질적인 데이터가 위치한 영역은 Unused 영역을 제외하면 D로 접근할 때가 제일 크기 때문에 struct mcount_arch_context 구조체의 필드명을 v가 아닌 d로 사용한 것으로 보임 (해당 내용이 맞는지 참고 자료 찾기)
-
다만, aarch64의 부동소수점 레지스터는 위에서 확인한 것과 같이 총 32개로 나오는데 왜 8개의 부동 소수점 레지스터만 관리할까 의문이 들었고, 몇 가지 조사를 통해 내린 결론은 아래와 같다.
- 함수 호출 시 인자나 함수의 반환 값이 부동 소수점 자료형인 경우 사용되는 레지스터는 V0 ~ V7에 해당하기 때문
- 이 부분은 추가적으로 “5-3-1. struct mcount_arch_context 구조체가 나오게 된 이유 분석”에서 uftrace의 이슈와 연관지어 설명할 예정
- RISC-V 아키텍처에는 RV32I, RV32E RV64I, RV128I 라고 하는 4가지의 ISA(Instruction Set Architecture)와 Extension이라는 개념이 존재하는데 추후 RISC-V 포팅을 위한 참고 자료나 문서를 볼 때 어떤 부분이 필요하고 필요하지 않은지 구분하기 위해서는 해당 내용이 필요하다고 판단해 해당 내용을 작성한다.
- RISC-V는 오픈소스 아키텍처이기 때문에 인텔, AMD, ARM과 같이 아키텍처를 공개하지 않는 CPU 분야에서 리눅스 커널과 같은 존재
- 하드웨어 칩을 설계하거나 제작할 능력을 가진 사람들은 RISC-V 아키텍처를 사용해 원하는 CPU를 만들 수 있다.
- 그렇다보니 기본적인 명령어들을 탑재한 ISA에 이후 서술할 Extension 들을 결합해 원하는 기능을 하드웨어 레벨에서 수행하는 CPU를 만들 수 있으며 아키텍처 명명 규칙이 조금 복잡하다는 점이 특징
- 보통은 RISC-V CPU라고 하겠지만, 아키텍처 명을 조금 더 세부적으로 들여다보게 되면 RV64GC 와 같은 문자열을 사용하는 것을 볼 수 있다.
- RV는 RISC-V의 아키텍처를 사용하는 경우 항상 붙는 이름이고, RV 다음에 붙은 숫자는 명령어가 처리하는 bit 수 를 의미하며, 그 다음에 오는 문자들은 RISC-V CPU를 만들 때 추가한 확장의 약어를 붙이게 된다.
-
RISC-V의 Extension은 위에서 잠깐 서술한 것과 같이 선택한 ISA에 미리 정의된 기능(=Extension)들 중 원하는 기능들을 블록을 결합하듯 추가하는 개념이라고 이해하면 된다.
-
저전력 임베디드용 RISC-V 아키텍처인 RV32E를 제외하고는 모두 I를 공통적으로 가지기 때문에 생략이 가능하고, 그 외에는 Extension의 약어를 붙이게 되어있다.
- 다만, 최근에는 사용되는 확장이 많아짐에 따라 자주 사용되는 확장들을 그룹화 한 “G”라는 확장을 정의해 사용하기도 한다.
- 자세한 확장 목록 및 설명은 아래 링크 참조
-
멘토님이 작성하신 문서에서는 RISC-V Ubuntu 23.04를 QEMU에서 실행하기 위해서는 -cpu 옵션에 sifive-u54를 줘야 한다고 되어있기 때문에, sifive-u54 CPU의 아키텍처 명명 규칙을 확인하기 위해 참조한 구조도는 아래 그림과 같다.
-
레지스터 부분에 한해서 아래 그림에서 볼 수 있는 것과 같이 RV32I, RV64I, RV128I 간 차이점은 레지스터의 크기 차이이며, 레지스터의 수와 역할은 동일한 것으로 보인다.
-
추후 uftrace가 RISC-V 32bit 를 지원할 수 있도록 포팅하는 작업을 진행하더라도 RISC-V 64bit의 파일들의 내용을 일부 가져다 사용할 수 있을 것으로 보여 포팅 작업이 조금 더 수월할 것으로 보여진다.
- RISC-V의 아키텍처 문서는 “Volume 1, Unprivileged Specification” 과 “Volume 2, Privileged Specification”으로 구성되며, 레지스터 목록과 관련된 내용은 “Volume 1, Unprivileged Specification”에 존재한다.
- 아래 링크에 접속해 “Volume 1, Unprivileged Specification” PDF 파일을 다운로드 받아야 한다.
-
RISC-V 아키텍처는 아래 그림의 표와 같이 x10 ~ x17에 해당하는 총 8개의 레지스터를 Argument 목적으로 사용하고 있다.
-
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; };
-
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
-
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 스크립트가 활성화 되면 부동 소수점 레지스터와 같은 일부 아키텍처 정보가 변경되기 때문에 발생하는 이슈를 해결하는 커밋으로 되어있다.
- 아래 커밋 링크에서 x86_64는 필드까지 구현하였지만, 나머지 아키텍처는 필드를 비워 둔 상태로 struct mcount_arch_context를 구현하였다. (다만, 이 때에는 i386은 지원하지 않아 arch 폴더에 i386은 존재하지 않음)
-
aarch64 아키텍처 이슈
- 아래 PR 링크에 따르면, aarch64에서 부동소수점을 사용하는 함수 인자나 반환 값이 잘못되는 문제가 있었고 이를 해결하는 코드 중 일부가 아래에 해당한다고 한다.
-
https://github.com/namhyung/uftrace/pull/1122
/* 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]; }; ......
-
- 아래 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 아키텍처의 부동 소수점 레지스터 분석이 필요할 것으로 보임
-
-
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 구조체가 정상적으로 동작하는지 확인이 필요하다.
-
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에 해당)
-
-
“C” Extension 이 사용되지 않는 경우 RV32I, RV64I, RV128I 모두 RV32I의 NOP 명령어 구조를 사용하기 때문에 아래와 같이 구성된다.
-
“C” Extension이 사용되지 않는 경우 RISC-V CPU의 NOP 명령어는 아래와 같이 구성되고, NOP 명칭 그대로 사용된다.(이때 NOP Instruction의 길이는 32bit기 때문에 4Byte에 해당)
-
-
결론적으로, “C” Extension이 사용될 때와 사용되지 않을 때 NOP 명령어의 크기에 차이가 있어 “C” Extension의 유무를 확인할 수 있는 방법이 필요하다.
- 현재 시스템에서 돌아가고 있는 RISC-V CPU의 아키텍처를 확인할 수 있는 방법은 Qemu 에서 돌아가는 RISC-V Ubuntu를 기준으로 아래와 같이 확인할 수 있었다.
-
cat
/proc/cpuinfo
명령을 실행한 결과로, isa라는 필드에서 extension 목록들이 표현되고 있었다. -
qemu가 아닌 실제 보드에서도 동일한 결과를 얻을 수 있는지 확인이 필요하다.
-
-
현재 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
- 다만, 위에서 서술한 extension을 확인할 수 있는 방법이 맞다면,
-
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 */
-
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 필드의 값만 추출하는 것을 확인할 수 있다.
-
-
이 내용은 추후 arch/riscv64 폴더 내부의 Makefile을 구현할 때 넣어아 하는지 아니면 최상위 디렉토리의 Makefile에 넣어야 하는지는 고민이 필요하다.
-
해당 내용이 실제 RISC-V 보드에서도 동일하게 적용되는지는 확인이 필요하다.