Stack Frame
-
frame pointer 레지스터는 linked list 구조처럼 스택에 있는 이전의 frame pointer의 주소를 저장한다. (마지막 프레임은 다음 프레임이 없으므로 0을 저장한다.)
-
stack의 frame pointer 앞에는 LR 레지스터의 값(리턴시 점프할 주소)도 같이 저장된다.
-
위와 같은 특성을 이용하면, stack frame을 순회하면서 frame pointer와 LR의 값들을 확인할 수 있다. 아래는 이 특성을 이용하여 구현한 단순한
dump_stack
함수이다. -
해당 코드를 ARM64에서 실행하면 아래와 같은 결과를 얻을 수 있다. frame pointer가 0에 도달할 때까지 stack frame을 순회한 것을 알 수 있다.
-
하지만 주소만 보여주는 것만으로는 정보를 파악하기 쉽지 않다. 디버깅을 위해선 주소에 대응하는 함수 이름도 같이 출력을 해줘야 한다.
KALLSYMS
- 리눅스에서는 커널의 심볼 정보들을 담당하는
kallsym
으로 함수 이름(심볼)을 가져올 수 있다. /proc/kallsyms
파일을 열면 현재 커널의 여러 심볼 정보들을 살펴볼 수 있다.
/scripts/kallsyms.c
)
Kallsyms에서 정보를 읽어오는 과정 (-
/scripts/kallsyms.c
라는 스크립트를 사용해 vmlinux의 심볼 정보들을 편하게 읽을 수 있다. -
해당 스크립트는 별도로 컴파일 되어 실행 가능한 파일이다. 입력으로는 파일의 심볼을 읽는
nm
유틸리티의 stdout을 사용한다. -
이 스크립트를 기준으로 kallsyms의 구조를 살펴보자.
-
kallsyms에서 주로 사용되는 구조체는 4개가 있다.
-
sym_entry
: 하나의 심볼에 대응하는 자료 구조이다. 심볼의 주소(addr
), 이름(sym[]
)을 저장한다. -
addr_range
: 어떤 영역에 대응하는 자료 구조 입니다. 영역의 시작과 끝에 대응하는 심볼과 주소를 저정한다. -
token_profit
: 2개의 문자로 이루어진 문자열의 빈도 수를 기록하는 테이블이다. 2개의 문자가 가질 수 있는 조합 수는0x10000
(=256x256) 이므로 테이블의 길이는0x10000
이다. -
best_table
: 압축에 사용되는 매핑 테이블이다. 각각의 char이 매핑되는 문자열을 저장한다.
-
-
addr_range는 아래와 같이 총 3개의 영역(text, init text, percpu)을 미리 정의한 뒤 실행된다. 각 영역의 시작과 끝에 대응하는 심볼은 알지만 그 주소는 아직 모르기 때문이다.
-
전체 과정은 크게 5단계로 나뉜다.
- symbol 파싱
- 유효하지 않은 심볼 삭제
- symbol entry 정렬
- symbol entry 압축
- symbol entry 출력
1. symbol 파싱
-
read_map
함수에서는 symbol을 파싱하고 테이블에 추가한다. -
symbol에 대한 핵심 파싱은
read_symbol
함수를 통해 이뤄진다. 파싱한 데이터는symbol_entry
의 형태로 반환되고, 테이블에 추가된다. -
해당 함수는 3가지의 일을 수행한다.
- 입력에서 심볼의 주소(addr), 타입(type), 심볼의 이름(name)을 받아온다.
- 받아온 정보들을 이용해
symbol_entry
를 생성 및 초기화한다. - 읽어온 심볼이
addr_range
에 시작과 끝에 해당하는 심볼이라면, 이 심볼의 주소를addr_range
에 저장한다.
2. 유효하지 않은 심볼 삭제
shrink_table
에서는 유효하지 않은symbol
들을 삭제한다.- 테이블을 순회하면서 invalid한 심볼들에 대해 free를 해줌으로써 valid한 symbol_entry만 남도록 한다.
3. symbol entry 정렬
-
symbol_entry
테이블 정렬은sort_symbol
함수에서 실행된다. -
compare_symbols
함수를 통해 qsort 방식으로 정렬을 수행한다. 정렬 기준은 주소이고, 주소가 동일한 경우 다른 속성을 비교한다.)
4. symbol entry 압축
-
symbol entry 압축은
optimize_table
함수에서 이뤄진다. -
optimize_table
함수는 아래와 같이 총 3개의 단계로 구성되어 있다. -
build_initial_tok_table()
:-
구성한
symbol_entry
테이블을 순회하면서learn_symbol
함수를 호출한다. -
learn_symbol
함수는symbol_entry
의sym
(type+name)과len
을 인자로 받는다. -
learn_symbol
은 받은 문자열symobl_entry.sym
을 순회하면서,char[2]
의 분포를token_profit
테이블에 반영한다.
-
-
insert_real_symbols_in_table()
-
insert_real_symbols_in_table
은symbol_entry
테이블을 순회하면서 sym에 사용되는 문자를 기록한다. -
한 번이라도 사용된 char은
best_table
에 기록되고, 사용되지 않은 char은 기록되지 않는다.
-
-
optimize_result()
-
optimize_result
는best_table
을 순회하면서 사용되지 않은 char을 찾고, 이 char을 빈번하게 사용된char[2]
와 매핑한다. -
빈번하게
char[2]
는 앞서 구성한token_profit
에서 가장 큰 값을 가진 것에 해당한다. -
그런 다음
symbol_entry
에 사용된 문자열을 매핑한 문자로 치환하는 작업을 수행한다.-
compress_symbols
함수에서 핵심 압축 로직을 수행한다. 첫 번째 인자는 압축할char[2]
이고, 두 번째 인자는 매핑된 1byte 정수이다. -
symbol_entry
테이블을 순회하면서 각symbol_entry.sym
에 압축 대상의 문자열이 존재하는지 확인한다. 만약 그렇다면, 해당 문자열을 idx로 치환한다. -
압축한
symbol_entry.sym
을 반영하기 위해 이전의 내용을 지우고(forget_symbol
), 압축이 완료된 후에는 다시learn_symbols
를 호출하여token_profit
을 최신으로 업데이트한다.
-
-
5. symbol entry 출력
-
write_src
에서는 assembly 파일 포맷에 맞춰 필요한 정보들을 출력한다. -
먼저 Archtiecture(64bit or 32bit)에 따라 매크로(
PTR
,ALGN
)를 정의한다. 심볼 관련 정보들은.rodata
섹션에 배치된다. -
그런 다음
symbol_entry
를 순회하면서 각각의 address를 출력한다.symbol_entry
의 갯수도kallsyms_num_syms
라는 이름으로 출력한다. 출력의 형식은 옵션에 따라 다르다. -
그런 다음
symbol_entry
의 sym을 출력한다.symbol_entry.sym
은 가변 길이이므로 검색의 용이성을 위해marker
라는 검색 인덱스를 만든다.marker
는 256개의symbol_entry.sym
마다 오프셋을 저장한다. -
최종적으로
0x00
에서0xFF
까지 순회하면서 char마다 대응하는 문자열 또는 char를 출력한다. 어떤 char은 재압축이 되었을 수도 있으므로expand_symbol
을 통해 압축을 해제한 문자열을 buf에 저장한다. -
이렇게 출력된 정보들은
kallsyms_token_table
에서 찾을 수 있다. -
정리하면,
write_src
는 다음과 같은 여러 정보들을 출력한다.kallsyms_address
: 심볼들의 주소kallsyms_num_syms
: symbol의 갯수kallsyms_names
: symbol들의 압축된 이름kallsyms_marker
:kallsyms_names
의 검색 인덱스kallsyms_token_table
: 압축된 문자(char)가 매핑 된 문자 또는 문자열
/scripts/link-vmlinux.sh
)
vmlinux 생성 관련 스크립트 (-
vmlinux를 생성하는 Makefile command에서는
/scripts/link-vmlinux.sh
라는 스크립트가 실행되는데,CONFIG_KALLSYMS
옵션이 활성화 되어 있다면 kallsyms 관련 일을 수행한다. -
link-vmlinux.sh
에서 사용되는 핵심 함수는vmlinux_link
와kallsyms
이다.-
vmlinux_link
: 첫 번째 인자로 받는 오브젝트 파일과vmlinux.o
를 링크하고, 두 번째 인자에서 받은 이름으로 출력 파일을 저장한다. -
kallsyms
: 첫 번째 인자로 받은 오프젝트 파일의 심볼 정보를 추출하고 어셈블리 파일으로 저장한다. 이때 저장하는 파일의 이름은 함수의 2번째 인자와 같다.
-
vmlinux.o
를 링크하여tmp_vmlinux1
이라는 임시 오브젝트 파일을 생성한다. 이 임시 오브젝트 파일은 kallsyms의 입력 파일로 제공되며,.tmp_kallsyms1.o
라는 중간 산출물 오브젝트 파일을 생성한다. 해당 중간 산출물 파일은 자신에 대한 심볼 정보(kallsyms_token_table
, ..)들을 포함하지 않는다. -
앞서 생성한
.tmp_kallsyms1.o
와vmlinux.o
를 링크하여.tmp_vmlinux2
라는 오브젝트 파일을 생성한다. 해당 오브젝트는 이전.tmp_vmlinux1
와 다르게kallsyms
관련 심볼들에 대한 올바른 정보를 포함하고 있다. 이렇게 생성한 오브젝트 파일을/script/kallsyms
의 입력으로 주어 최종적인 심볼 관련 오브젝트 파일.tmp_kallsyms2.o
를 생성한다. -
tmp_kallsyms1.o
와.tmp_kallsyms2.o
의 크기가 다르다면 변환 단계를 추가로 실행한다. -
최종적으로
vmlinux.o
와 생성한 최종 오브젝트를 링크하여vmlinux
파일을 생성한다.
-
kernel/kallsyms.c
심볼 정보 API -
해당 파일에서는 생성한 여러 심볼 정보를 사용하는 여러 API를 제공한다.
-
리눅스 커널에서 사용되는 표준 출력 함수인
printk()
의 포인터 관련 추가 기능에도 내부적으로kallsyms
의 API가 사용된다. -
핵심 함수로
__sprint_symbol
이 있다.
-
주소에 대응하는 정보를 조회하는
kallsyms_lookup_buildid
함수를 살펴보자. -
get_symbol_pos()
함수는 조사하려는 주소가 심볼들의 주소를 저장했던kallsyms_address
라는 테이블에서 몇 번쨰 인덱스에 해당하는지 탐색한다. -
주소에 대응하는 인덱스를 구하는
get_symbol_pos
함수는 이진 탐색으로 구현되었다. 앞서 심볼들의 주소를 저장할 때 정렬을 했기 때문에 이진 탐색이 가능하다. -
다만, 몇몇 심볼들은 동일한 주소를 가지고 있기에 해당 함수의 크기와 오프셋을 구하기 위해 추가적인 루틴이 존재한다.
-
구한 인덱스로 압축된 문자열의 주소를 구하기 위해
get_symbol_offset()
함수를 사용한다. -
다음으로,
kallsyms_expand_symbol()
함수에서는 구한 pos에 위치한 압축된 문자열을 압축 해제한다. 만약 조사하는 주소가 모듈, bpf, ftrace에 속한다면 별도의 처리를 수행한다. -
압축 해제를 위해선
kallsyms_token_table
을 사용한다.
참고
- https://github.com/torvalds/linux/blob/2c8159388952f530bd260e097293ccc0209240be/scripts/link-vmlinux.sh#L148
- https://stackoverflow.com/questions/20196636/does-kallsyms-have-all-the-symbol-of-kernel-functions
- https://www.bhral.com/post/stacktrace%EC%99%80kallsyms%EC%9D%98%EA%B5%AC%ED%98%84%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0