Skip to content

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

최기철 edited this page Aug 27, 2023 · 1 revision

1. mcount.S를 구현하기 위한 사전 지식

mcount.S를 구현하기 위한 사전지식 참조

2. 다른 아키텍처의 mcount.S 파일 분석

2-1. [x86_64 아키텍처] mcount.S에 구현된 mcount 함수 분석

2-1-1. x86_64아키텍처의 스택 공간 확보 부분

  • 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
    
    	......

2-2. [aarch64 아키텍처] mcount.S에 구현된 mcount 함수 분석

2-2-1. aarch64 아키텍처의 스택 공간 확보 및 Frame Pointer 저장 부분

  • stp 어셈블리 명령어는 3번째 인자에 붙은 ! 여부에 따라 동작 방식이 달라지는데, !가 붙었을 때 아래 어셈블리 코드는 아래와 같이 동작한다.

    1. sp(stack pointer 레지스터)의 메모리 주소 위치를 -16Byte만큼 이동 (#-16 부분은 오프셋 값을 의미하는 상수이기 때문에 변경 가능)
    2. 현재 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
    
    	......

2-2-2. aarch64 아키텍처의 함수 인자 저장 부분

  • 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]!
    
    	......

2-2-3. aarch64 아키텍처에서 mcount_entry 함수를 호출하는 부분

  • 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

	......

2-2-4. aarch64 아키텍처에서 함수의 인자들을 복원하는 부분

  • 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
    
    	......

3. RISC-V 64bit의 mcount.S 구현

3-1. mcount 함수의 이름 확인하기

  • 각 아키텍처의 mcount.S 파일을 보면 GLOBAL(mcount), GLOBAL(*mcount)*, GLOBAL(__gnu_mcount_nc) 와 같이 mcount 함수가 호출되었을 때 사용될 어셈블리 언어의 함수 이름이 정의된 것을 확인할 수 있다.
  • 이 부분은 gcc 컴파일러와 같이 컴파일러가 사용한 mcount 함수의 이름을 확인해야 하며, 아래와 같은 방법으로 확인할 수 있었다.

3-1-1. 크로스 컴파일 환경에서 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
    ```

3-1-2. 네이티브 환경에서 mcount 함수의 이름 확인하기

- RISC-V 64bit 가상 환경 내부에서 확인하고자 한다면, 아래 명령어를 사용하여 `-pg` 옵션으로 컴파일 한 뒤 `objdump` 명령어를 사용하여 확인한다.
    
    ```bash
    $ gcc -pg -o riscv-test helloworld.c
    $ objdump -d riscv-test
    ```

3-1-3. objdump 명령으로 확인된 결과 (예시)

- 아래는 크로스 컴파일 환경에서 실행된 `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>
    ```

3-2. [RISC-V 64bit 아키텍처] mcount.S에 mcount 함수 구현

3-2-1. RISC-V 64bit 아키텍처의 스택 공간 확보 및 Frame Pointer 저장 부분

  • 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)에 이전 스택과 현재 스택의
                                   // 경계가 되는 위치를 넣어주기 위해 확보한 공간만큼
                                   // 더해서 저장

3-2-2. RISC-V 64bit 아키텍처의 함수 인자 저장 부분

	......

  /* 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 인자를 스택에 저장

  ......

3-2-2. RISC-V 64bit 아키텍처의 mcount_entry 함수 호출 부분

  ......

  /* 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

  ......

3-2-3. RISC-V 64bit 아키텍처의 함수 인자 복원 부분

  ......

  /* 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)

3-3. [RISC-V 64bit 아키텍처] 현재까지 구현한 _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

3-3-1. small_tracer의 mcount.S 파일 수정

  • 기존에 존재하는 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

3-3-2. small_tracer의 main.c 수정

  • 기존의 코드에서 수정해야 할 함수는 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);
}

3-3-3. 컴파일 방법

  • 아래 명령어를 사용하여 mcount.S 파일과 main.c가 하나의 파일로 컴파일 될 수 있도록 한다.
    • 여기서 uftrace와 차이가 있는게 uftrace는 mcount.s가 타겟 프로그램과 분리되어 있는데 여기서는 타겟 프로그램 내부에 포함하여 빌드함으로써 복잡한 과정 없이 바로 검증이 가능하였다.

      gcc -pg mcount.S main.c

3-3-4. 실행 결과

  • 실행 파일을 실행하면 아래와 같이 나와야하며, main(), bar(), foo() 3개의 함수에 각각 _mcount 함수가 적용되었기 때문에 아래와 같이 메시지도 3개가 출력된다.
    • 여기서는 _mcount 어셈블리어 함수만 테스트를 진행하였지만, 동일한 방법으로 mcount_return 어셈블리어 함수도 테스트 할 수 있을 것으로 보인다.
Clone this wiki locally