-
Notifications
You must be signed in to change notification settings - Fork 0
[RISC‐V 64] 2. mcount.S 구현 과정
-
rsp 레지스터는 x86_64의 Stack Pointer Register이고,
$48 앞의 $ 는 해당 값이 Hex 값을 나타내는 기호이다. -
sub $48, %rsp
명령에 의해 Stack Pointer가 가리키는 주소로 부터 0x48만큼 감소시킨다.GLOBAL(mcount) .cfi_startproc sub $48, %rsp .cfi_adjust_cfa_offset 48 ......
-
stp 어셈블리 명령어는 3번째 인자에 붙은
!
여부에 따라 동작 방식이 달라지는데,!
가 붙었을 때 아래 어셈블리 코드는 아래와 같이 동작한다.- sp(stack pointer 레지스터)의 메모리 주소 위치를 -16Byte만큼 이동 (#-16 부분은 오프셋 값을 의미하는 상수이기 때문에 변경 가능)
- 현재 sp 위치에 x29 값을 저장, 현재 sp+0x8 위치에 x30 값을 저장
- 2-2-1. aarch64 아키텍처의 정수 레지스터 목록 에서 확인한 그림에 따르면, x29는 Frame Pointer 레지스터이고 x30은 Link Register로 반환 주소를 가지고 있는 레지스터이다.
- 그러므로, 해당 부분은 Frame Pointer 와 Return Address를 스택에 저장하고, 현재 Stack Pointer의 주소를 Frame Pointer 레지스터에 저장하는 부분이다.
GLOBAL(_mcount) /* setup frame pointer */ stp x29, x30, [sp, #-16]! mov x29, sp ......
-
aarch64 아키텍처의 함수 인자 레지스터 수는 총 8개이기 때문에, 8개의 함수 인자들을 스택에 저장한다.
-
stp 어셈블리 명령어는 한번에 2개의 레지스터를 3번째 인자로 주어진 공간에 저장하는 명령어로, 동작 구조는 위에서 설명한 것과 같다.
- x0, x1부터 저장하지 않는 이유는 스택이 동작하는 구조는 FILO(First In, Last Out) 구조이기 때문이고, 함수가 인자를 스택에서 꺼내갈 때 x0, x1부터 나오게 하기 위해서 제일 마지막에 저장한다. (해당 부분은 참고 링크 찾아서 작성하기)
...... /* save arguments */ stp x6, x7, [sp, #-16]! stp x4, x5, [sp, #-16]! stp x2, x3, [sp, #-16]! stp x0, x1, [sp, #-16]! ......
- mcount_entry 함수를 호출하기 전 함수 호출에 사용될 인자를 설정하는 부분으로, 실질적으로 mcount_entry 함수에서 사용될 인자들을 의미한다.
- 각 어셈블리 코드에 대한 설명은 아래에 주석으로 작성하였다.
......
// x29 레지스터에는 현재 sp가 가리키고 있는 주소가 저장되어 있기 때문에,
// x0 레지스터에 sp의 값을 로드한다.
ldr x0, [x29]
// x0 레지스터에 담긴 주소 값에 0x8을 더해 다시 x0 레지스터에 저장한다.
add x0, x0, #8
// return address 주소를 담고있는 레지스터인 x30의 값을 x1 레지스터에 복사한다.
mov x1, x30
// 현재 stack pointer의 주소를 x2 레지스터에 복사한다.
mov x2, sp
// mcount_entry 함수를 호출한다.
bl mcount_entry
......
-
mcount_entry 함수의 역할은 끝났기 때문에 호출을 위해 사용한 스택 공간을 정리해야 하며, 아래 부분이 해당되는 부분이다.
...... /* restore arguments */ ldp x0, x1, [sp], #16 ldp x2, x3, [sp], #16 ldp x4, x5, [sp], #16 ldp x6, x7, [sp], #16 ...... /* restore frame pointer */ ldp x29, x30, [sp], #16 ......
- 각 아키텍처의 mcount.S 파일을 보면
GLOBAL(mcount)
,GLOBAL(*mcount)*
,GLOBAL(__gnu_mcount_nc)
와 같이 mcount 함수가 호출되었을 때 사용될 어셈블리 언어의 함수 이름이 정의된 것을 확인할 수 있다. - 이 부분은 gcc 컴파일러와 같이 컴파일러가 사용한 mcount 함수의 이름을 확인해야 하며, 아래와 같은 방법으로 확인할 수 있었다.
- [[Linux] RISC‐V 64bit 개발환경 구축](https://github.com/kosslab-kr/uftrace/wiki/%5BLinux%5D-RISC%E2%80%90V-64bit-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95) 에서 구축한 크로스 컴파일 환경이라면, `-pg` 옵션으로 컴파일 한 뒤 `objdump` 명령어를 사용하여 확인한다.
```bash
$ riscv64-unknown-linux-gnu-gcc -pg -o riscv-test helloworld.c
$ riscv64-unknown-linux-gnu-objdump -d riscv-test
```
- RISC-V 64bit 가상 환경 내부에서 확인하고자 한다면, 아래 명령어를 사용하여 `-pg` 옵션으로 컴파일 한 뒤 `objdump` 명령어를 사용하여 확인한다.
```bash
$ gcc -pg -o riscv-test helloworld.c
$ objdump -d riscv-test
```
- 아래는 크로스 컴파일 환경에서 실행된 `objdump` 실행 결과의 예시로, 네이티브 환경인 RISC-V 64bit 가상 환경이나 보드에서 실행한 결과는 다를 수 있다.
- 아래에서 확인된 결과로는 **컴파일러에 의해 사용되는 mcount의 함수명은 _mcount 인 것을 확인**할 수 있다.
```bash
Disassembly of section .plt:
......
0000000000010600 <puts@plt>:
10600: 00002e17 auipc t3,0x2
10604: a28e3e03 ld t3,-1496(t3) # 12028 <puts@GLIBC_2.27>
10608: 000e0367 jalr t1,t3
1060c: 00000013 nop
0000000000010610 <_mcount@plt>:
10610: 00002e17 auipc t3,0x2
10614: a20e3e03 ld t3,-1504(t3) # 12030 <_mcount@GLIBC_2.27>
10618: 000e0367 jalr t1,t3
1061c: 00000013 nop
Disassembly of section .text:
0000000000010620 <_start>:
10620: 022000ef jal 10642 <load_gp>
10624: 87aa mv a5,a0
10626: 00002517 auipc a0,0x2
1062a: a2253503 ld a0,-1502(a0) # 12048 <main@@Base+0x1960>
1062e: 6582 ld a1,0(sp)
10630: 0030 add a2,sp,8
10632: ff017113 and sp,sp,-16
10636: 4681 li a3,0
10638: 4701 li a4,0
1063a: 880a mv a6,sp
1063c: f95ff0ef jal 105d0 <__libc_start_main@plt>
10640: 9002 ebreak
......
00000000000106e8 <main>:
106e8: 1141 add sp,sp,-16
106ea: e406 sd ra,8(sp)
106ec: e022 sd s0,0(sp)
106ee: 0800 add s0,sp,16
106f0: 8786 mv a5,ra
106f2: 853e mv a0,a5
**106f4: f1dff0ef jal 10610 <_mcount@plt>**
106f8: 67c1 lui a5,0x10
106fa: 72078513 add a0,a5,1824 # 10720 <_IO_stdin_used+0x8>
106fe: f03ff0ef jal 10600 <puts@plt>
10702: 4781 li a5,0
10704: 853e mv a0,a5
10706: 60a2 ld ra,8(sp)
10708: 6402 ld s0,0(sp)
1070a: 0141 add sp,sp,16
1070c: 8082 ret
000000000001070e <atexit>:
1070e: 8581b603 ld a2,-1960(gp) # 12058 <__dso_handle>
10712: 4581 li a1,0
10714: b5f1 j 105e0 <__cxa_atexit@plt>
```
-
RISC-V 64bit 아키텍처의 Frame Pointer와 Stack Pointer 정보는 4-2-1. RISC-V 아키텍처의 정수 및 부동 소수점 레지스터 목록 을 참조하면 된다.
- .cfi 지시문이 포함되어야 하는지는 이슈로 물어보기
#include "utils/ash.h" .text GLOBAL(_mcount) /* setup frame pointer & return address */ addi sp, sp, -80 /* stack frame에서 80byte만큼 공간 확보 */ sd ra, 72(sp) // return address 값을 sp + 0x8 위치에 저장 sd s0, 64(sp) // s0는 frame pointer register 이므로, // fp의 값을 sp + 0x8 위치에 저장 ...... (함수 인자 저장 부분) addi s0, sp, 80 // frame pointer register(s0)에 이전 스택과 현재 스택의 // 경계가 되는 위치를 넣어주기 위해 확보한 공간만큼 // 더해서 저장
......
/* save arguments */
sd a0, 56(sp) // a0 인자를 스택에 저장
sd a1, 48(sp) // a1 인자를 스택에 저장
sd a2, 40(sp) // a2 인자를 스택에 저장
sd a3, 32(sp) // a3 인자를 스택에 저장
sd a4, 24(sp) // a4 인자를 스택에 저장
sd a5, 16(sp) // a5 인자를 스택에 저장
sd a6, 8(sp) // a6 인자를 스택에 저장
sd a7, 0(sp) // a7 인자를 스택에 저장
......
......
/* parent location */
ld t0, 64(sp)
mv a0, t0
/* child addr */
mv a1, ra
/* mcount_args */
mv a2, sp
/* call mcount_entry func */
call mcount_entry
......
......
/* restore argunents */
ld a7, 0(sp) // 스택에 저장된 여덟 번째 인자를 a7 레지스터에 로드
ld a6, 8(sp) // 스택에 저장된 일곱 번째 인자를 a6 레지스터에 로드
ld a5, 16(sp) // 스택에 저장된 여섯 번째 인자를 a5 레지스터에 로드
ld a4, 24(sp) // 스택에 저장된 다섯 번째 인자를 a4 레지스터에 로드
ld a3, 32(sp) // 스택에 저장된 네 번째 인자를 a3 레지스터에 로드
ld a2, 40(sp) // 스택에 저장된 세 번째 인자를 a2 레지스터에 로드
ld a1, 48(sp) // 스택에 저장된 두 번째 인자를 a1 레지스터에 로드
ld a0, 56(sp) // 스택에 저장된 첫 번째 인자를 a0 레지스터에 로드
/* restore frame pointer */
ld s0, 64(sp) // fp 레지스터에 스택에 저장된 frame pointer 값 로드
ld ra, 72(sp) // ra 레지스터에 스택에 저장된 return address 값 로드
addi sp, sp, 80 /* stack frame에서 80byte만큼 공간 해제 */
ret
END(_mcount)
-
위에서 만든 _mcount 어셈블리 코드를 바로 uftrace 프로젝트에 적용하여 테스트가 불가능하기 때문에 다른 방법을 사용하여 구현된 어셈블리 코드가 맞는지 검증하였다.
-
https://github.com/YWHyuk/small_tracer.git 의 코드를 가져와 위에서 작성한 mcount.S로 교체하고, main.c 파일을 일부 수정하였다.
-
아래의 명령을 실행하여 git clone 수행
git clone https://github.com/YWHyuk/small_tracer.git
- 기존에 존재하는 mcount.S 파일은 x86_64 아키텍처를 기준으로 되어있기 때문에, 위에서 만든 RISC-V 아키텍처용으로 변경해야 한다.
- 대부분의 코드는 동일하지만, 함수를 선언하는 부분과 같이 일부 다른 부분이 있기 때문에 기존 형식을 유지하면서 작성된 RISC-V 아키텍처용 mcount.S는 아래와 같다.
.text
.global _mcount
_mcount:
addi sp, sp, -80
sd ra, 72(sp)
sd s0, 64(sp)
# 인자 저장 부분
sd a0, 56(sp)
sd a1, 48(sp)
sd a2, 40(sp)
sd a3, 32(sp)
sd a4, 24(sp)
sd a5, 16(sp)
sd a6, 8(sp)
sd a7, 0(sp)
addi s0, sp, 80
# 함수 호출 부분 (원래는 맞는 값을 설정해서 넘겨야 하지만,
# 함수의 인자를 임의로 0으로 설정하였음)
#
# 또한, dummy_ftrace_func는 나중에 uftrace의 mcount_entry를 호출하는
# 부분으로 바뀔 예정
mv a0, zero
mv a1, zero
call dummy_ftrace_func
#인자 복원 부분
ld a7, 0(sp)
ld a6, 8(sp)
ld a5, 16(sp)
ld a4, 24(sp)
ld a3, 32(sp)
ld a2, 40(sp)
ld a1, 48(sp)
ld a0, 56(sp)
ld s0, 64(sp)
ld ra, 72(sp)
addi sp, sp, 80
ret
- 기존의 코드에서 수정해야 할 함수는 dummy_ftrace_func() 함수이며, 이 함수는 python 을 실행하여 화면에 정보를 출력하는 구조로 되어있다.
- 하지만, 우리는 mcount.S가 정상적으로 동작하는지만 검증하기 위한것이기 때문에 아래와 같이 코드를 수정하였다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void __attribute__((no_instrument_function)) dummy_ftrace_func(unsigned long ip, unsigned long parent_ip)
{
printf("dummy_ftrace_func, ip:%ld, parent_ip:%ld\n", ip, parent_ip);
//char buffer[1024];
//sprintf(buffer, "python3 symbol.py --pid %d --addr1 %p --addr2 %p", getpid(), (void*)ip, (void*)parent_ip);
//system(buffer);
}
int foo()
{
printf("foo() \n");
return 1;
}
int bar()
{
printf("bar() \n");
return foo();
}
int recursive(int a)
{
if(!a)
return 0;
recursive(a-1);
}
int main()
{
printf("main() \n");
bar();
//recursive(4);
}
- 아래 명령어를 사용하여 mcount.S 파일과 main.c가 하나의 파일로 컴파일 될 수 있도록 한다.
-
여기서 uftrace와 차이가 있는게 uftrace는 mcount.s가 타겟 프로그램과 분리되어 있는데 여기서는 타겟 프로그램 내부에 포함하여 빌드함으로써 복잡한 과정 없이 바로 검증이 가능하였다.
gcc -pg mcount.S main.c
-
- 실행 파일을 실행하면 아래와 같이 나와야하며, main(), bar(), foo() 3개의 함수에 각각 _mcount 함수가 적용되었기 때문에 아래와 같이 메시지도 3개가 출력된다.
- 여기서는 _mcount 어셈블리어 함수만 테스트를 진행하였지만, 동일한 방법으로 mcount_return 어셈블리어 함수도 테스트 할 수 있을 것으로 보인다.