AArch64 프로세서의 스택 언와인딩: 정의 및 작동 방식
KernelCare Enterprise의 Linux Kernel 라이브 패치 소프트웨어는 한동안 x86_64(Intel IA32/AMD AMD64) 외에 ARMv8(AArch64)도 지원했습니다. 그러나 KernelCare 실행에서 실행하려면 스택 프레임 언와인더라는 것이 필요합니다.
이 글에서는 스택 프레임이 무엇인지, 어떤 용도로 사용되는지, 왜 스택 프레임 언와인더를 직접 작성해야 했는지 설명합니다.
- 스택이 풀립니다: 스택 언와인더의 정의, 용도 및 역사
- 스택 언와인딩 작동 방식: 스택 프라이머와 AArch64 프로세서에서 작동하는 방식
- 스택 언와인더에 대한 지침: 점프할 지침, 점프할 주소 및 문제 해결
파트 A - 스택 언와인더
스택 언와인더란 무엇인가요?
A 스택 언와인더 는 현재 호출 스택에 있는 모든 함수의 주소를 나열하는 소프트웨어입니다. 프로그램 실행의 현재 위치를 보여주지만 더 중요한 것은 어떻게 거기에 도달했는지도 보여줍니다. 호출 스택은 현재 실행 중인 모든 함수의 목록입니다. 스택이라고 부르는 이유는 한 함수가 다른 함수를 호출하면 현재 실행 중인 함수가 스택 위에 새 함수가 추가되기 때문입니다. 함수가 값을 반환하거나 종료 명령에 도달하면 해당 함수는 스택에서 제거됩니다.
효과적인 언와인더는 이러한 특성을 모두 갖추고 있어야 합니다:
- 빠름로 설정하여 처리를 빠르게 재개할 수 있도록 합니다(가능한 경우).
- 저렴로 설정하여 시스템 리소스를 소모하지 않습니다.
- 정확성메모리 주소와 네임스페이스가 정확하게 보고되도록 합니다.
스택 언와인더는 어떤 용도로 사용되나요?
- 프로그램이 충돌할 때 스택 추적을 제공합니다. (Linux Kernel 세계에서는 이러한 충돌을 '웁스'라고 부릅니다.)
- 프로그램 내에서 프로그램이 어떤 함수를 호출하는지 경로를 표시하면 성능 분석에 도움이 됩니다.
- 시스템을 중지(재부팅)하지 않고 Kernel 버그를 수정하는 작업인 Linux Kernel 라이브 패치를 사용하려면 다음과 같이 하세요.
스택 언와인딩의 역사
역사적으로 스택 언와인딩은 개발자가 소프트웨어를 디버깅하는 데 도움이 되었습니다. 프로그램을 몇 개라도 작성해 본 사람이라면 누구나 프로그램에 오류가 있을 가능성이 높다는 것을 알고 있습니다.
어떤 오류는 쉽게 발견할 수 있는 반면 어떤 오류는 거의 눈에 띄지 않습니다. 프로그램의 규모가 클수록 디버깅하기가 더 어려워집니다. 대규모 프로그램은 소스 코드 분석만으로는 디버깅이 거의 불가능합니다.
그렇기 때문에 디버깅 프로세스를 용이하게 하기 위한 몇 가지 보조 기술이 있습니다:
- 로깅(즉, 디버깅 출력). 여기서 프로그래머는 자신이 선택한 언어의 인쇄 연산자를 사용하여 프로그램 흐름의 특정 지점에서 특정 변수의 내용을 표시합니다. 이를 통해 프로그램 흐름이 예상 시나리오에서 벗어난 지점을 파악할 수 있습니다. 최신 소프트웨어는 로깅에 크게 의존하지만 디버그, 알림, 경고, 오류 등 다양한 로깅 수준을 사용하여 프로덕션 환경에서 덜 중요한 일부 로깅 메시지를 비활성화할 수 있습니다. 일반적으로 로깅 메시지는 전용 로그 파일로 이동하거나 시스템 전체 로그 저장소 또는 파일로 이동합니다.
- 중단점을 통한 디버깅. 디버깅 프로세스를 용이하게 하기 위해 거의 모든 최신 CPU 아키텍처에는 "중단점" 명령어라는 특수 명령어가 포함되어 있습니다(예: ARM의 경우 bkpt, x86의 경우 int3). 이 명령어의 목적은 특수 프로세서 인터럽트를 발생시키는 것입니다. 그러면 하드웨어는 현재 프로그램 카운터를 저장하고 인터럽트 서비스 루틴이 성공적으로 시작되도록 돕기 위해 몇 가지 범용 레지스터를 저장할 수 있습니다. 그런 다음 제어권이 인터럽트 서비스 루틴으로 전송됩니다.
이 루틴은 범용 레지스터의 저장된 내용을 추출하여 프로그래머가 프로그램이 중지된 현재 지점에서 특정 프로그램 변수를 검사하는 데 도움을 줍니다. 특수 인터럽트 서비스 루틴 리턴 명령을 실행하기 직전에 이 인터럽트 핸들러는 일반적으로 원래 명령을 복원하고 중단된 스레드로 돌아간 후에는 프로그램이 전혀 건드리지 않은 것처럼 실행됩니다.
오늘날 이 기술은 모든 인터럽트 서비스 루틴이 운영 체제 Kernel의 일부이기 때문에 운영 체제 Kernel의 지원에 의존하여 사용됩니다. 일반적으로 OS Kernel은 사용자 공간 프로세스가 디버깅 기능을 수행하는 데 사용할 수 있는 특수 시스템 호출(예: Linux의 ptrace)을 제공합니다. 널리 사용되는 Linux 오픈 소스 디버거인 GDB는 (중단점을 통해) 단계별 디버깅을 수행하기 위해 ptrace를 사용합니다.
또한 일부 CPU 아키텍처(예: x86-64)는 원래 아이디어에 기반한 추가 기능을 정의합니다. 예를 들어 (가상) 중단점 주소가 포함된 특수 시스템 레지스터가 있을 수 있는데, 프로그램 카운터가 이 주소에 도달하면 프로그램 코드를 패치(결과적으로 복원)할 필요 없이 인터럽트 서비스 루틴이 즉시 호출됩니다. - 어설션을 통한 디버깅. 어설션은 주어진 조건을 검사하고 이 조건이 거짓일 경우 프로그램을 종료하는 특수 함수입니다. 프로그래머는 내부 변수가 정상인지 확인하기 위해 어설션을 사용할 수 있습니다. 일반적으로 프로덕션 프로그램에서는 어설션이 비활성화되어 있습니다. 여기서 가장 흥미로운 경우는 어설션이 거짓일 때입니다. 이 상태는 프로그램 오류가 발생했음을 분명히 나타냅니다. 문제를 조사하기 위해 스택 언와인딩이 수행됩니다. 프로그램의 스택을 풀면 프로그램이 충돌한 지점까지 이어지는 '실행 경로'를 얻을 수 있습니다.
고급 언와인더를 사용하면 호출 체인 내의 각 함수에 대한 매개변수를 확인할 수 있습니다.
따라서 스택 언와인딩은 원래 소프트웨어 디버깅에서 시작되었습니다. 하지만 지금은 다른 용도로도 사용되고 있으며, 그 중 하나가 Kernelcare Enterprise에 있습니다.
KernelCare 엔터프라이즈 팀에 ARM용 언와인더가 필요했던 이유
Linux Kernel 라이브 패치를 설치하려면 패치 소프트웨어가 현재 호출 스택에 어떤 함수가 있는지 알고 있어야 합니다. 현재 호출 스택에 있는 함수가 패치되면 반환할 때 시스템이 충돌할 수 있습니다. Linux Kernel에는 이미 일부 스택 언와인딩 기능이 있습니다. 다음은 해당 기능에 대한 간략한 리뷰와 라이브 패치에 사용할 수 없는 이유입니다:
- '추측'이 풀립니다: 스택의 내용을 추측합니다. 정확하지 않으므로 라이브 패치에는 유용하지 않습니다. x86_64 아키텍처에서만 사용할 수 있습니다.
- '프레임 포인터' '프레임 포인터' 풀기: x86_64에서만 사용 가능
- The 'ORC' 풀기: 도입된 버전 Linux Kernel v4.14에서 "ORC"는 "죄송합니다 되감기 기능"의 약어입니다. 원래 x86_64 전용으로 개발되었지만 계속 개선되어 Kernel에 포함될 패치가 고려되고 있습니다(Linux Kernel 6.3 기준). ARM으로 지원을 확장하고 정확한 정보를 반환하기 위해 안정성 검사를 추가하는 패치가 고려되고 있습니다.
파트 B - 스택 언와인딩 작동 방식
스택 프라이머
- 함수가 호출되면 스택 프레임 은 함수의 인자와 진입점 및 종료점을 추적합니다.
- 프로세서 레지스터에는 스택에 가장 최근에 놓인 객체를 참조하는 스택 포인트('SP')가 할당됩니다. 해당 스택을 구현하는 메모리는 낮은 메모리 주소를 향해 아래쪽으로 확장됩니다(소위 '풀 디스카운딩').
- 스택 메모리는 바이트 경계에 맞춰 정렬되어야 합니다(AArch64의 경우 16바이트). 이는 하드웨어에 의해 강제됩니다(일부 ARM 모델에서는 비활성화될 수 있음).
세부 정보
AArch64 프로세서에서 스택 언와인딩은 어떻게 작동하나요?
특수 Kernel 함수는 스택 언와인딩을 수행합니다. 이 함수가 호출되면 호출하는 함수의 프레임 포인터(FP)를 가져옵니다. FP는 스택 프레임을 가리키며, 이는 구조체 stack_frame으로 표현됩니다. 여기에는 호출 함수를 호출한 함수의 스택 프레임에 대한 포인터가 포함됩니다.
즉, 다음에 획득한 FP가 0이 되면 끝나는 스택 프레임의 링크된 목록이 있습니다. AAPCS64 (AArch64의 프로시저 호출 표준)에 따라 끝나는 링크드 리스트가 있다는 뜻입니다. 각 스택 프레임에서 호출 함수가 작업을 완료한 후 제어권을 위임해야 하는 반환 주소를 검색할 수 있습니다. 반환 주소가 호출 함수 내부를 가리켜야 한다는 사실을 이용해 FP=0이 되는 지점까지 모든 함수의 심볼릭 이름을 얻을 수 있습니다.
이를 위해 함수의 이름과 시작 및 종료 주소를 보관합니다. 이는 kallsyms라는 Linux Kernel 서브시스템을 사용하여 구현할 수 있습니다.
핵심 문제는 다음과 같이 요약할 수 있습니다. 어떻게 하면 프로그램 카운터를 한 곳에서 다른 곳으로 이동(점프)하고 문제 없이 처리를 재개할 수 있을까요?
이 동작은 어셈블리 언어로 다음과 같이 표현할 수 있습니다:
이에 대해 좀 더 자세히 살펴보겠습니다.
1. 점프 지침
프로시저는 BL로 호출됩니다. 32비트 명령어는 다음과 같이 시각화할 수 있습니다:
31 30 29 28 27 26 25 (...) 2 1 0
1 0 0 1 0 1 imm26
op
여기서 imm26은 26비트 PC 오프셋입니다. 이 예제에 대해 자세히 알아보려면 ARM 설명서( 여기.
2. 이동하려는 주소
어디로 이동하여 사용할지 계산합니다:
bits(64) offset = SignExtend(imm26:'00', 64)
The offset shifts by two bits to the left and converts to 64 bit (i.e. the high bits fill with 1 if imm26 < 0, and with 0, otherwise).
그러면 이동할 주소가 표시됩니다:
Address = PC + offset
오프셋은 BL 명령어에 레이블이 지정되어 사용됩니다. (AArch64의 명령어는 항상 4바이트를 사용하므로 x86에 비해 이점이 있습니다.) 레지스터 X30(링크 레지스터라고도 함)은 PC+4로 설정됩니다. 이것은 RET의 리턴 주소입니다(지정하지 않으면 기본값은 X30입니다).
따라서 보완 명령어 RET의 경우 저장된 LR 값을 검색하고 제어권을 이 값으로 전송한 후 호출 함수로 반환하는 것으로 충분합니다.
문제: 호출된 함수가 다른 함수 자체를 호출하면 어떻게 될까요?
호출된 함수가 다른 함수 자체를 호출하면 어떻게 될까요? 아무것도하지 않으면 LR에 저장된 값이 새 반환 주소로 대체되어 초기 함수로 돌아갈 수없고 프로그램이 중단 될 가능성이 높습니다.
문제 해결
이 문제를 해결할 수 있는 몇 가지 방법이 있습니다:
- LR 값을 다른 레지스터에 저장합니다.
- LR 값을 RAM에 저장
첫 번째 경우는 사용 가능한 레지스터 수가 31개로 제한되어 있기 때문에 매우 제한적입니다. ARM은 메모리 호출은 LD(로드) 및 ST(스토어) 명령어를 통해 수행되는 반면, 산술 및 논리 연산은 레지스터에서 수행되는 RISC 아키텍처 로드/스토어 아키텍처 철학을 사용합니다. 따라서 프로그램 실행을 위해서는 빈 레지스터가 필요하며, LR 값을 RAM에 저장하는 옵션 2가 남게 됩니다.
호출된 함수를 스택에 저장하여 반환 주소 저장하기
C로 구현된 프레임 구조는 다음과 같습니다:
struct stack_frame {
unsigned long fp;
unsigned long lr;
char data[0];
};
즉, 각 함수는 스택에 n 바이트를 할당하여 BL 명령어로 제어권을 가져오는 순간 스택 포인터를 n만큼 줄입니다. 레지스터 x29(FP) 및 x30(LR)의 내용은 획득한 스택 포인터 값(이 값에 사용되는 호출 함수)에 따라 저장됩니다. 그 후 새 값 SP가 레지스터 x29(프레임 포인터(FP)라고 함)에 할당됩니다. 스택 프레임의 나머지 공간은 함수 로컬 변수에 의해 사용됩니다. 그리고 호출 함수의 프레임 포인터(FP)와 링크 레지스터(LR)가 항상 스택 프레임의 시작 부분에 위치해야 한다는 조건이 항상 충족됩니다. 호출된 함수는 작업을 완료한 후 스택 프레임에서 저장된 FP와 LR 값을 가져와 스택 포인터(SP)를 n만큼 증가시킵니다.
gcc 크로스 컴파일러가 AArch64를 처리하는 방법
프레임 포인터를 사용하려면 Linux Kernel을 -fno-omit-frame-pointer gcc 옵션으로 컴파일해야 합니다. 이 옵션은 스택 프레임 포인터를 레지스터에 저장하도록 gcc에 지시합니다. (참고: gcc의 기본값은 -fomit-frame-pointer이므로 이 옵션을 명시적으로 설정해야 합니다.)
AArch64의 경우 레지스터는 X29입니다. 이 레지스터는 옵션이 설정된 경우 스택 프레임 포인터를 위해 예약됩니다. (그렇지 않으면 다른 용도로 사용할 수 있습니다.) AArch64에서 Linux를 컴파일하는 데 사용되는 크로스 컴파일러 GCC는 함수 본문 앞에 다음 지침을 설정합니다:
ffffff80080851b8 :
ffffff80080851b8: a9be7bfd stp x29, x30, [sp, #-32]!
ffffff80080851bc: 910003fd mov x29, sp
여기서 스택 포인터(SP)를 처음에 32씩 감소시킨 다음 첫 번째 명령어에서 얻은 값만큼 x29, x30을 순차적으로 메모리에 저장하는 사전 증가를 이용한 간접 주소 지정이 사용됩니다.
일반적으로 함수는 다음과 같이 완료됩니다:
ffffff80080851fc: a8c27bfd ldp x29, x30, [sp], #32
ffffff8008085200: d65f03c0 ret
스택 포인터(SP)의 메모리에서 저장된 값 x29, x30을 가져온 다음 SP를 32만큼 증가시키는 사후 증가를 사용한 간접 주소 지정입니다. 위의 코드 예제를 각각 함수의 프롤로그와 에필로그라고 부릅니다. 플래그 -fno-omit-frame-pointer가 설정되어 있으면 GCC는 항상 이러한 프롤로그와 에필로그를 생성합니다. AArch64의 Linux는 해당 플래그를 사용하여 컴파일되므로 스택 프레임이 일반 코드(어셈블리 코드 제외)처럼 보입니다. 이 사실 덕분에 스택을 쉽게 풀 수 있습니다. 즉, 프로그램에서 호출 체인을 추적할 수 있습니다.
어셈블리 참조:
결론
스택 언와인딩의 유용성에도 불구하고 모든 아키텍처와 시스템을 포괄하는 스택 언와인딩에 대한 일반적인 접근 방식은 없습니다. Linux Kernel 라이브 패치의 경우 안정적이고 빠른 스택 언와인딩이 필수적입니다. ARM에서 실행되는 Linux의 경우, ARM이 IoT 디바이스 및 엣지 클라우드 컴퓨팅 시장에서 주목을 받으면서 강력한 스택 언와인딩 솔루션의 필요성이 더욱 절실해졌습니다. KernelCare가 두 가지 모두에 적용되면서 Kernel 스택 언와인딩을 위한 자체 솔루션을 찾아야 했습니다.
추가 읽기
- "AArch64에서 스택 사용: 푸시 앤 팝 구현" - Jacob Bramley, 2015년 11월 @ Arm 개발자 커뮤니티 프로세서 블로그
- "AArch32 및 AArch64에서 스택 사용" - Jacob Bramley, 2015년 11월 @ Arm 개발자 커뮤니티 프로세서 블로그
- "Linux x86 ORC 스택 언와인더" - 매트 플레밍, 2017년 7월 @ Code Blueprint
- "ORC 언와인더" - Josh Poimboef, 2017년 7월 @ LWN.net
- 제안된 ORC 업데이트
- Linux Kernel 스택 유효성 검사
- Linux Kernel 라이브 패치 - 라이브 패치를 위한 일반적인 일관성 모델.
- ARM A64 명령어 세트 아키텍처 - BL 명령어
KernelCare Enterprise 정보
KernelCare Enterprise를 사용하면 CentOS, Amazon Linux, RHEL, Ubuntu, Debian, 그리고 기타 Linux 배포판(Poky(요크토 프로젝트의 배포판) 및 Raspbian 포함) 서버에 간편하게 패치를 적용할 수 있습니다.
KernelCare는 서비스 중단이나 성능 저하 없이 재부팅 없이 자동화된 업데이트를 통해 Kernel 보안을 유지합니다. 이 서비스는 실행 중인 Kernel에 자동으로 적용되는 다양한 Linux 배포판의 최신 보안 패치를 단 몇 나노초 만에 신속하게 제공합니다. KernelCare Enterprise는 라이브 및 스테이징 환경은 물론 로컬 및 온클라우드 시스템에서 모두 작동하며, 방화벽 뒤에 위치한 서버의 경우 온프레미스 ePortal 도구로 관리할 수 있습니다.
KernelCare Enterprise는 금융 및 보험 서비스, 화상 회의 솔루션 제공업체, 가정 폭력 피해자를 보호하는 회사, 호스팅 회사, 공공 서비스 제공업체 등 서비스 가용성과 데이터 보호가 비즈니스에서 가장 중요한 부분인 다양한 기업의 수십만 대 서버에서 규정 준수를 강화합니다.
KernelCare Enterprise와 이 제품이 제공하는 혜택에 대해 자세히 알아보세요. 여기에서.