호출 규약(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 레지스터를 다음과 같이 정의합니다.
#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 함수 호출 규약을 참고하시기 바랍니다.