[Arm 아키텍처] 어셈블리 명령어로 무엇을 할 수 있을까
새로운 주제를 배우기 전에 배운 내용이 어떻게 활용되는지 알면 큰 동기 부여가 됩니다. 이는 어셈블리 명령어를 배울 때도 적용됩니다.
부트로더를 개발할 때
시스템 소프트웨어를 개발할 때 많이 익히는 모듈 중 하나는 부트로더입니다. 어떤 컴퓨터나 휴대폰이던 시스템에 전원이 인가되면 전원이 Off된 상태에서 최초로 실행되는 소프트웨어가 있는데, 이를 부트로더라고 합니다. 그런데 부트 로더에서 가장 먼저 실행되는 코드가 스타트업 코드인데, 스타트업 코드는 어셈블리 명령어를 입력해 구현해야 합니다.
스타트업 코드에서 시스템을 설정하는 세부 구현 방식은 다르지만 대부분 다음과 같은 초기화 작업을 수행합니다.
스택 초기화
MMU 설정
시스템 레지스터 설정
익셉션 벡터 베이스 주소 설정
이해를 돕기 위해 리눅스에서 많이 활용되는 LK(Little Kernel) 부트로더의 스타트업 코드를 소개합니다.
https://github.com/littlekernel/lk/blob/master/arch/arm64/start.S
01 .section .text.boot
02 FUNCTION(_start)
03 .globl arm_reset
04 arm_reset:
05 bl arm64_elX_to_el1
...
06 ldr tmp, =__stack_end
07 mov sp, tmp
08
09 /* clear bss */
10 .L__do_bss:
11 /* clear out the bss excluding the stack and kernel translation table */
12 / * NOTE: relies on __post_prebss_bss_start and __bss_end being 8 byte aligned */
13 ldr tmp, =__post_prebss_bss_start
14 ldr tmp2, =__bss_end
위 코드와 같이 부트로더의 스타트업 코드는 어셈블리 명령어로 구성돼 있는데, 코드의 주요 동작을 요약하면 다음과 같습니다.
06~07번째 줄: 스택 설정
10~14번째 줄: BBS 영역 초기화
---
[정보] BSS란
여기서 BSS는 'Block Started by Symbel'의 약자로 메모리 영역 중 하나입니다. 초기값과 함께 선언되지 않는 전역변수는 BSS 영역에 할당됩니다.
---
또한 부트로더에서 하드웨어를 초기화(브링업)하는 루틴은 주로 어셈블리 명령어로 구현됩니다. 따라서 어셈블리 명령어를 구성하는 기본 포멧이나 동작 원리를 알고 있어야 부트로더를 잘 개발할 수 있습니다.
Arm 아키텍처의 기능을 활용해 드라이버를 구현해야 할 때
Arm 아키텍처의 기능을 활용해 주어진 모듈을 구현하려면 어셈블리 명령어를 입력해야 합니다.
한 가지 예를 들어 볼까요? Arm 아키텍처에서 지원하는 보완 확장(Security Extensions)인 트러스트 존의 기능을 활용하려면 SMC(Secure Monitor Call) 명령어를 실행해야 합니다. 또한 시스템 콜을 구현하려면 SVC(Supervisor Call) 명령어를 입력해, 유저 공간에서 커널 공간으로 진입할 수 있게 해야 합니다.
다음은 리눅스 커널에서 제공하는 SMC 명령어와 관련된 인터페이스 함수입니다.
https://elixir.bootlin.com/linux/v5.15/source/arch/arm64/kernel/smccc-call.S
/*
* void arm_smccc_smc(unsigned long a0, unsigned long a1, unsigned long a2,
* unsigned long a3, unsigned long a4, unsigned long a5,
* unsigned long a6, unsigned long a7, struct arm_smccc_res *res,
* struct arm_smccc_quirk *quirk)
*/
01 SYM_FUNC_START(__arm_smccc_smc)
02 SMCCC smc
02번째 줄을 보면 smc 명령어를 실행하는 코드가 확인됩니다. smc 명령어의 동작 원리를 알아야 관련 코드의 목적을 이해할 수 있습니다.
Performance를 최적화하기 위해서
프로젝트를 진행하다보면 대부분 성능 이슈를 겪습니다. 예를 들어 소프트웨어 Q/A 인증 팀에서 다음과 같은 현상이 있다고 제보(버그 리포트)합니다.
가끔 소리가 끊긴다.
애플리케이션의 반응 속도가 느리다.
시스템(개발용 샘플)이 빨리 뜨거워진다.
이런 성능 이슈를 해결하려면 먼저 문제의 원인을 파악해야 합니다. 그 다음 성능을 최적화하는 코드를 작성하면 성능 문제를 해결할 수 있습니다. 그런데 성능을 최적화 하기 위해서도 어셈블리 명령어를 사용해야 하는 경우가 있습니다.
한 가지 예를 들어 볼까요? WFI, WFE와 같은 명령어는 Arm 코어가 스탠 바이(저전력) 모드에 진입하는 기능을 지원하는데, 이 명령어를 사용하면 소모 전력을 최대한 줄일 수 있습니다.
디버깅을 잘 하기 위해서
프로젝트를 진행하다보면 다양한 문제를 만나며, 이를 일정 내에 해결하기 위해서는 문제 원인을 제대로 파악해야 합니다. 이 과정에서 디버깅을 하는데 어셈블리 명령어의 동작 원리를 잘 알아야 디버깅을 잘 할 수 있습니다.
이번에 다음과 같이 커널 패닉이 일어났을 때의 메시지를 보면서 디버깅하는 과정을 소개하겠습니다.
01 [71653.365758] Unable to handle kernel NULL pointer dereference at virtual address 000001bc
02 [71653.365775] pgd = c0004000
03 [71653.365785] [000000a8] *pgd=00000000
04 [71653.365805] Internal error: Oops: 5 [#1] PREEMPT SMP ARM
05 [71653.365841] Modules linked in: bcmdhd(O)
06 [71653.365864] CPU: 0 Tainted: G O (3.4.65-g000d394 #1)
07 [71653.365891] PC is at tty_wakeup+0x8/0x80
08 [71653.365910] LR is at gs_start_io+0x90/0xe8
09 [71653.365922] pc : [<c04f29d8>] lr : [<c04a2520>] psr: 20030193
10 [71653.365928] sp : d2b2db10 ip : d2b2db28 fp : d2b2db24
11 [71653.365944] r10: e29ba318 r9 : e29ba324 r8 : 00000000
12 [71653.365955] r7 : e29ba2f4 r6 : e16290a8 r5 : e29ba2e8 r4 : e29ba2c0
13 [71653.365967] r3 : 0000000a r2 : 00000000 r1 : 60030193 r0 : 00000000
14 [71653.365984] Flags: nzCv IRQs off FIQs on Mode SVC_32 ISA ARM Segment kernel
15 [71653.365996] Control: 10c5387d Table: 8567c04a DAC: 00000015
위 커널 로그에서 가장 중요한 메시지는 다음과 같이 01번째 줄, 07번째 줄과 09번째 줄입니다.
01 [71653.365758] Unable to handle kernel NULL pointer dereference at virtual address 000001bc
07 [71653.365891] PC is at tty_wakeup+0x8/0x80
09 [71653.365922] pc : [<c04f29d8>] lr : [<c04a2520>] psr: 20030193
01번째 줄은 "000001bc 가상 주소를 처리할 수 없다"라는 사실을 알려줍니다. 07번째와 09번째 줄 메시지에서 "c04f29d8 주소에서 데이터 어보트가 발생했다"라는 정보를 알 수 있습니다.
데이터 어보트가 발생한 원인을 정확히 파악하려면 데이터 어보트가 발생한 주소의 코드를 분석할 필요가 있습니다. 이어서 데이터 어보트가 유발된 c04f29d8 주소에 있는 어셈블리 코드를 분석하겠습니다.
01 c04f29cc <tty_wakeup>:
02 c04f29cc: e1a0c00d mov ip, sp
03 c04f29d0: e92dd830 push {r4, r5, fp, ip, lr, pc}
04 c04f29d4: e24cb004 sub fp, ip, #4
05 c04f29d8: e59031bc ldr r3, [r0, #444] ; 0x1bc
05번째 줄에 보이는 ' ldr r3, [r0, #444]' 명령어를 실행하다가 데이터 어보트가 유발됐음을 알 수 있습니다. 그런데 데이터 어보트가 유발된 0xc04f29d8 주소에 있는 ldr 명령어의 동작 원리를 알아야 데이터 어보트가 유발된 원인을 알 수 있습니다.
어셈블리 명령어를 입력해 특정 기능을 구현하는 것도 중요하지만, 어셈블리 명령어 분석을 잘 하면 디버깅을 잘 할 수 있습니다. 또한 다른 개발자가 작성한 코드를 더 잘 이해할 수 있습니다.
.