호출 규약(Calling Convention)이란 무엇일까요? 어떤 함수를 호출했을 때 서브 루틴이 자신을 호출하는 함수로부터 인자를 어떤 방식으로 받아 결과를 반환하는지에 대한 규약입니다. x86, RISC-V와 같은 CPU 아키텍처마다 함수 호출 규약(Calling Convention)을 정의하는데, ARM 프로세서에서는 이를 AAPCS(Procedure Call Standard for ARM Architecture)라고 명시합니다. 앞으로, AAPCS를 함수 호출 규약이라고 명시하겠습니다.
 
연산을 수행하는 ARM 어셈블리 명령어에서 레지스터는 연산의 결과를 임시로 저장하는 역할에 그칩니다. 그래서 각각 레지스터의 역할을 세세하게 배울 필요가 없는데요. 함수 호출 규약을 배울 때는 다릅니다. 그것은 다음과 같은 이유 때문입니다. 
 
    "함수 호출 규약의 핵심은 레지스터입니다." 
 
함수 호출 규약을 정의할 때는 레지스터가 각각 어떤 역할을 수행하는지 세세하게 다룹니다.
 
ARMv7 아키텍처의 함수 호출 규약
 
다음 테이블은 ARMv7 아키텍처에서 각각 레지스터의 역할을 나타냅니다.
 

 
표 1.5 ARMv7 아키텍처의 함수 호출 규약에서 레지스터의 역할 
 
ARMv7 아키텍처 기준으로 호출 규약에서 16개의 ARM 레지스터를 다음과 같이 정의합니다. 
 
   ● r15: 프로그램 카운터 
   ● r14: 링크 레지스터 
   ● r13: 스택 포인터 
   ● r12: 스크래치 용으로 사용되는 레지스터 
   ● r4~r11: 지역 변수를 저장을 위해 사용 
   ● r0~r3: 함수로 전달되는 인수값을 저장하고 함수로부터 반환되는 결과값을 r0 레지스터에 저장
 
특히 r14, r13(sp: 스택 포인터) 레지스터의 정보는 디버깅을 할 때 자주 사용되는 레지스터로 함수 호출 규약에서 중요한 역할을 수행합니다.
 
이어서 다음 그림을 보면서 ARMv8 아키텍처에서 각각 레지스터의 역할을 알아봅시다.
 

 
그림 1.6 ARMv8 함수 호출 규약에서 레지스터의 역할
 
위 그림은 ARMv8 아키텍처의 함수 호출 규약에서 각각 레지스터의 역할을 나타내는데, 각각 레지스터의 역할은 다음과 같습니다.
 
   ● X0~X8: 함수에 아규먼트를 전달할 때 사용
   ● X9~X15: 함수를 호출할 때 지역 변수를 저장 
   ● X29: 프레임 포인터 레지스터로 이전 스택 포인터 주소를 저장
   ● X30: 링크 레지스터
 
ARMv7 아키텍처와 마찬가지로 ARMv8 아키텍처의 함수 호출 규약에서는 X30(링크 레지스터)와 X29(프레임 포인터) 레지스터가 중요한 역할을 맡습니다. 디버깅을 할 때 자주 참고하는 레지스터입니다.
 
ARMv7/ARMv8 아키텍처의 함수 호출 규약에서 레지스터는 중요한 역할을 수행합니다. 
ARM의 함수 호출 규약을 실전 프로젝트에서 어떻게 활용될까?
 
ARM의 함수 호출 규약을 배우면 생기는 의문점을 말씀 드리려 합니다.
 
   ● 도대체 ARM 아키텍처의 함수 호출 규약은 실전 프로젝트에서 어떻게 활용될까?
 
저를 포함해 많은 개발자들이 품는 의문입니다. 이런 의문을 해소하기 위해 실전 디버깅 프로그램에서 확인한 콜 스택(함수의 호출 흐름을 출력)을 소개합니다.
 
다음은 크래시 유틸리티 프로그램에서 확인한 콜 스택 출력 결과입니다.
 
crash> bt  
PID: 4944   TASK: d110e040  CPU: 0   COMMAND: "sh"
 #0 [<c01003ac>] (do_DataAbort) from [<c0fde6a0>]
    pc : [<c0524b88>]    lr : [<c0fdde2c>]    psr: 60010013
    sp : c59c3f10  ip : c1920700  fp : bee5765c
    r10: 00000000  r9 : acf2a284  r8 : 00000000
    r7 : 00000063  r6 : 00000000  r5 : c191ff18  r4 : c195cd70
    r3 : 00000000  r2 : 00000001  r1 : 40010013  r0 : 00000000
    Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM
 #1 [<c0fde6a0>] (__dabt_svc) from [<c0fdde2c>]
 #2 [<c0524b88>] (sysrq_handle_crash) from [<c0525034>]
 #3 [<c0525034>] (__handle_sysrq) from [<c0525468>]
 #4 [<c0525468>] (write_sysrq_trigger) from [<c028a310>]
 #5 [<c028a310>] (proc_reg_write) from [<c0237b70>]
 #6 [<c0237b70>] (vfs_write) from [<c0238078>]
 #7 [<c0238078>] (sys_write) from [<c0106b00>]
 
#7번째 줄에서 #1번째 줄 방향으로 함수가 호출된 것을 나타내는데, 함수가 호출된 흐름이 깔끔하게 출력됩니다.
 
크래시 유틸리티는 리눅스 커널를 디버깅하기 위한 오픈 소스 프로젝트인데, 실전 프로젝트에서 많이 활용됩니다.
 
이번에는 GDB 프로그램으로 본 콜 스택 출력 결과입니다.
 
(gdb) bt
#0  0xb6fd7d5c in do_lookup_x (undef_name=0x7c9c7b11) at dl-lookup.c:541
#1  0xb6fd88b4 in _dl_lookup_symbol_x (undef_name=0x1023b "puts") at dl-lookup.c:814
#2  0xb6fdda54 in _dl_fixup (l=0xb6fff978) at dl-runtime.c:112
#3  0xb6fe3b44 in _dl_runtime_resolve () at ../sysdeps/arm/dl-trampoline.S:57
#4  0x00010414 in main () at main.c:5
 
보다시피 함수가 호출된 흐름을 깔끔하게 보여줍니다. 함수의 호출 흐름은 #4번째 줄에서 #0번째 줄 방향입니다.
 
이처럼 크래시 유틸리티와 GDB와 같은 프로그램을 사용할 때 당연히 함수가 호출된 흐름을 잘 표현해줄 것이라 예상합니다. GDB 프로그램이 함수의 콜 스택을 정확히 표현해주는 이유는 무엇일까요? 
 
   ● 크래시 유틸리티와 GDB 프로그램이 ARM의 함수 호출 규약에 따라 함수의 실행 흐름을
      표현하기 때문입니다.
 
사실 대부분 개발자들은 C 언어로 코드를 작성할 때 함수를 호출합니다. 단지 함수를 호출하지 않고 인자를 전달하면서 호출합니다. 그리고 호출된 함수가 반환하는 값으로 제어하는 코드를 입력합니다. 코드를 입력해 컴파일을 하면 당연히 원하는 동작을 수행하는데, 그 이유는 다음과 같습니다.
 
   ● 모두 ARM 프로세서의 함수 호출 규약에 따라 코드가 작동하기 때문이다.
 
소프트웨어 개발자로 함수 호출 규약은 반드시 잘 익혀둬야 할 핵심 개념입니다. 함수 호출 규약에 대한 상세한 내용과 실습은 5장 ARM 함수 호출 규약을 참고하시기 바랍니다.
 
< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>
 
 
* 유튜브 소개 영상

+ Recent posts