일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- Kaggle
- Dreamhack
- Computer Architecture
- 백준
- BAEKJOON
- fastapi를 사용한 파이썬 웹 개발
- bloc
- Algorithm
- 파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습
- ARM
- system hacking
- study book
- 영상처리
- Flutter
- FastAPI
- BFS
- Image Processing
- DART
- C++
- llm을 활용 단어장 앱 개발일지
- Stream
- MDP
- ML
- BOF
- PCA
- MATLAB
- pytorch
- Widget
- rao
- Got
- Today
- Total
Bull
[ARM] test_and_set_bit() : 커널 크래시 덤프 분석 본문
크래시 덤프 분석
크래시가 발생하면 운영체제는 메모리 덤프를 생성한다. 이 덤프 파일은 크래시 당시의 메모리 상태를 기록한 파일로 분석하여 문제의 원인을 파악할 수 있다. 윈도우 시스템에서는 우리가 흔히 아는 Blue Screen of Death(BSOD) 가 있으며 보통 .dmp
파일 형태로 덤프가 저장된다. 리눅스 가은 경우 kexec
나 kdump
를 통해 커널 덤프를 생성할 수 있다. /var/log/messages
혹은 /var/log/kern.log
와 같은 로그 파일에서 커널 메시지를 확인할 수 있다.
콜 스택 분석
콜 스택은 크래시 시점의 함수 호출 순서를 나타낸다. 콜스택을 통해 어떤 함수에서 오류가 발생했는지 알 수 있다. 예를 들어 다음과 같은 콜 스택이 있다고 가정하자.
-000|do_DataAbort()
-001|__dabt_svc(asm) -->|exception
-002|test_and_set_bit()
-003|bit_spin_lock(inline)
-003|hlist_bl_lock(inline)
...
이 콜 스택은 test_and_set_bit()
함수가 호출되었을 때 Data Abort 예외가 발생했음을 알 수 있다. 공부를 하던 도중 나의 지식으로 일반 코드에서 예외(익셉션)를 발생시키기 위한 함수로 비유된 것으로만 보인다. 우선 기초 지식이 없기 때문에 test_and_set_bit() 이 어떻게 생겼고 그 코드만을 먼저 알아보고자 하였다.
test_and_set_bit
이 함수는 리눅스 커널에서 사용되는 유틸리티함수로 특정 비트의 상태를 테스트하고 그 비트를 원자적 작업을 수행한다. 주로 멀티스레드 환경에서 동기화 관련된 작업에 자주 사용되며 목적은 Race Condition
을 방지하는 것이다.
static inline int test_and_set_bit(int nr, volatile unsigned long *addr);
nr
: 테스트하고 설정할 비트의 인덱스이다.addr
: 비트맵이 저장된 주소이다.
함수가 호출되면 addr 이 가리키는 비트맵에 nr 번째에 해당하는 비트를 검사한다. 해당 비트가 0이면 1로 설정 후 0을 반환한다. 1이면 상태를 유지하고 1을 반환한다.
unsigned long bit_map = 0b00000000000000000000000000000000;
위 주소라고 할 때 nr 이 3(0번째부터)이면 설정 후 비트 상태는 0b00000000000000000000000000001000
이 된다.
주로 커널 내부에 lock을 구현하거나 여러 스레드가 공유하는 자원에 대한 접근을 제어할 때 사용한다.
define CODE
코드를 제대로 살펴보겠다.
https://elixir.bootlin.com/linux/v6.10-rc1/source/arch/alpha/include/asm/bitops.h#L130에서 확인할 수 있다.
bitops.h - arch/alpha/include/asm/bitops.h - Linux source code v6.10-rc1 - Bootlin
/ arch / alpha / include / asm / bitops.h /* SPDX-License-Identifier: GPL-2.0 */ #ifndef _ALPHA_BITOPS_H #define _ALPHA_BITOPS_H #ifndef _LINUX_BITOPS_H #error only can be included directly #endif #include #include /* * Copyright 1994, Linus Torvalds. */ /
elixir.bootlin.com
static inline int
test_and_set_bit(unsigned long nr, volatile void *addr)
{
unsigned long oldbit;
unsigned long temp;
int *m = ((int *) addr) + (nr >> 5);
__asm__ __volatile__(
#ifdef CONFIG_SMP
" mb\n"
#endif
"1: ldl_l %0,%4\n"
" and %0,%3,%2\n"
" bne %2,2f\n"
" xor %0,%3,%0\n"
" stl_c %0,%1\n"
" beq %0,3f\n"
"2:\n"
#ifdef CONFIG_SMP
" mb\n"
#endif
".subsection 2\n"
"3: br 1b\n"
".previous"
:"=&r" (temp), "=m" (*m), "=&r" (oldbit)
:"Ir" (1UL << (nr & 31)), "m" (*m) : "memory");
return oldbit != 0;
}
volatile
: 컴파일러가 최적화 하지 않도록 한다. 보통 외부에서 메모리 접근을 최적화하기 위해 변수의 값을 레지스터에 저장해두고 메모리에서 값을 다시 읽지 않도록 할 수 있는데 일르 막는 것이다. 메모리의 값이 외부에서 변경할 것이기 때문이다. 컴파일러는 항상 이 변수를 메모리에서 직접 읽게 된다.
static inline
: 여러 파일에 같은 함수가 정의되어도, 이 함수가 내부적으로만 사용되고 각 파일에서 독립적으로 인라인화되도록 한다.
local variable
unsigned long oldbit;
unsigned long temp;
int *m = ((int *) addr) + (nr >> 5);
oldbit
: 선택된 비트의 기존 값을 저장한다.temp
: 어셈블리 코드에서 임시로 사용할 변수이다.m
: addr 을 시작으로 몇 번째에 속한 바이트가 되는 지 계산한다. nr>>5
는 nr/32
로 나눈 몫이다. 예를 들어 64 bit가 있다고 할 때 43번째가 읽고 싶다면 43/32=1
만큼 인덱스를 건너 뛴다. 즉 32bit 단위로 읽고 32+11번째 bit를 읽는다.
| 0000 0000 0000 0000 0000 0000 0000 0000 | | 0000 0000 0001 0000 0000 0000 0000 0000 |
|-------------------0---------------------| |-------------------1---------------------|
*m
은 1이니까 addr을 시작 지점으로 1 인덱스에서 11번째를 읽게 되는 것이다. addr이 어느 메모리에 위치해 있든, 예를들어 343번째 bit도 바꿀 수 있겠지만 특정 범위 이상이 되면 막히지 않을까 생각한다. 왜냐하면 보안상으로 문제가 될 수도 있기 때문이다.
Assembly
#ifdef CONFIG_SMP
" mb\n"
#endif
mb
: memory barrier로 SMP(Symmetric Multiprocessing, 다중 프로세서 시스템)에서 메모리 접근 순서를 보장하기 위해 사용된다.
"1: ldl_l %0,%4\n" // 메모리에서 값을 로드 (ldl_l 명령어는 원자적 로드)
" and %0,%3,%2\n" // 특정 비트(`nr`)를 테스트
" bne %2,2f\n" // 해당 비트가 1이면 2로 점프
ldl_l %0, %4
: 메모리 위치 m(%4)에서 값을 로드하여 temp(%0)에 저장한다.
and %0, %3, %2
: 로드된 값 temp(%0)와 (1UL << (nr & 31))(%3)과 and 연산 후 결과를 oldbit(%2)에 저장한다.
만약 nr이 3번째라면,nr & 31
: 0011,1UL << 0011
: 0110,temp & 0110
: 0000, oldbit에는 0이 들어간다.
bne %2, 2f
: oldbit가 0이 아니면 (비트가 설정되어 있다면) 레이블을 2로 점프한다.
" xor %0,%3,%0\n" // 비트를 반전 (설정 또는 해제)
" stl_c %0,%1\n" // 설정된 값을 메모리에 저장 (stl_c 명령어는 원자적 저장)
" beq %0,3f\n" // 저장 실패 시 레이블 3으로 점프
xor %0,%3,%0
: temp(%0) = (1UL << (nr & 31))(%3) ^ temp (%0).
stl_c %0,%1
: m(%1) = temp(%0), 메모리 위치에 값을 저장한다. 조건부 저장으로 성공하려면 해당 메모리 위치가 다른 프로세서에 의해 수정되지 않은 상태여야한다. 그래서 ldl_l
와 함께 사용한다.
beq %0,3f
: stl_c 명령어를 실패하면 레이블 3으로 간다.
".subsection 2\n"
"3: br 1b\n" // 저장이 실패하면 다시 1번으로 돌아가 반복
".previous"
br 1b
: 1번 레이블로 다시 점프하여 과정을 반복한다.
.subsection 2
: 2번 하위 섹션으로 이동시킨다.
.previous
: 2번 하위 섹션 반복을 종료하고 원래 코드 흐름으로 되돌린다.
오퍼랜드
__asm__ __volatile__(
:"=&r" (temp), "=m" (*m), "=&r" (oldbit) // 출력 오퍼랜드
:"Ir" (1UL << (nr & 31)), "m" (*m) : "memory"); // 입력 오퍼랜드
출력/입력 오퍼랜드를 각 순서대로 %0
,%1
,%2
,%3
,%4
로 매핑된다. 즉%0
: temp%1
: m%2
: oldbit%3
: 1UL << (nr & 31)%4
: m"memory"
: 어셈블리 코드가 메모리 상태를 변경할 수 있음
r
: 일반 레지스터(register), 일반적인 CPU 레지스터에 할당되어야함을 의미.=r
: 쓰기 전용 레지스터(출력 오퍼랜드), 쓰기 전용 오퍼랜드로 할당된다.=&r
: 조기 클로버(early clobber) 제약, 해당 오퍼랜드가 어셈블리 명령어가 실행되기 전에 다른 입력 오퍼랜드와 겹쳐서는 안됨을 의미, 데이터 보장, 데이터 무결성을 유지.(출력 오퍼랜드)
m
: 메모리 오퍼랜드, 해당 오퍼랜드는 메모리 위치를 참조해야함.Ir
: 즉시값(Immediate register) 혹은 레지스터 오퍼랜드로 사용할 수 있음.
'Computer Science > ARM' 카테고리의 다른 글
[ARM::kernel] 어셈블리에서 레이블을 왜 자주 사용하는 걸까? (C에서는 피하라고 배웠었다) (3) | 2024.09.01 |
---|---|
[ARM] 프로세서 모드와 레지스터 | study book (4) | 2024.08.28 |
[ARM] 프로세서 명명법 | study book (3) | 2024.08.28 |