티스토리 뷰
TLS(Thread Local Storage) : 각 스레드 별로 다른 값을 가지는 전역 변수, 기본적으로 일반적인 전역 변수는 모든 스레드가 공유하기 때문에 상호배제를 안 걸면 Race Condition이 발생 가능하기 때문에 사용(물론, 일반 전역 변수에 직접 상호배제를 걸어도 됨)
TLS 변수 선언 방법
1 2 3 4 5 | // .tbss Section __thread int val1; // .tdata Section __thread int val2 = 0x12345678 | cs |
ELF Format에는 TLS 전역 변수를 저장하기 위해 .data/bss 섹션이외에 별도로 .tdata/tbss 섹션이 추가됨
초기화 이미지 과정
- ELF Format에 .tdata/tbss 섹션 추가
- 링크 과정에서 PT_TLS 타입의 데이터 세그먼트랑 합쳐짐(초기화 이미지)
- 스레드를 생성할 때마다 초기화 이미지로 TLS를 초기화
(모든 스레드의 초기 값 동일)
TLS 변수를 찾아가는 과정
- GS 레지스터는 TCB를 찾아가기 위해 GDTR의 6번 항목인 TLS 전용 Segment Descriptor를 가리킴
- TLS Segment Descriptor의 Base Address 항목에는 TCB의 시작 위치가 저장되어 있고, 시작에는 헤더인 구조체 tcbhead_t가 있음
- 구조체 tcbhead_t에는 dtv 포인터 변수가 있는데, dtv_t 배열의 주소를 저장하고 있음
- dtv 배열의 0번은 generation number라고 하여, dtv 배열 크기 - 2(하나는 0번 항목, 하나는 모르겠음...)이 저장됨
ex) 위 같은 경우는 dtv 배열 크기가 3이므로, generation number가 1임
dtv 배열의 1번부터 TLS 변수의 시작 위치와 관련되어 가리키는 pointer 구조체로 저장됨 - is_static(True이면 static, False이면 dynamic)으로 구분한 후, TLS 변수의 시작 위치인 val을 따라 이동
- TLS 변수의 시작 위치 도착
TLS 변수 찾아가는 과정(상세 설명)
운영체제에서는 프로세스와 스레드를 관리하기 위해 이들에 대한 정보를 저장해 놓는 구조체를 가진다. 이런 구조체를 PCB(Process Control Block), TCB(Thread Control Block)이라고 한다.
리눅스는 프로세스와 스레드를 task_struct 구조체 하나로 관리한다.(참고로 윈도우는 EPROCESS, ETHREAD 구조체로 따로 나누어 관리) 따라서 스레드를 위한 task_struct 구조체가 TCB가 된다. TLS는 스레드별로 각자의 변수를 가지기 때문에 스레드를 관리할 때 알려줘야 한다. 그래서 TCB 내에는 로드된 각 모듈의 TLS 영역의 시작 주소를 가지고 있는 DTV(Dynamic Thread Vector) 포인터가 있다.
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 | typedef struct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; unsigned long int vgetcpu_cache[2]; # ifndef __ASSUME_PRIVATE_FUTEX int private_futex; # else int __unused1; # endif int rtld_must_xmm_save; /* Reservation of some values for the TM ABI. */ void *__private_tm[5]; long int __unused2; /* Have space for the post-AVX register size. */ __m128 rtld_savespace_sse[8][4] __attribute__ ((aligned (32))); void *__padding[8]; } tcbhead_t; | cs |
구조체 tcbhead_t의 5번째 줄을 보면 dtv라고 되어 있는 변수가 있는데, 이것이 TLS 영역의 시작 주소를 가지고 있는 DTV 포인터이다.
DTV 포인터는 dtv_t 공용체로 이루어진 배열의 시작 주소를 가지고 있는데, 공용체의 내용은 아래와 같다(구조체인 struct가 아니라 공용체인 union). 0번 항목은 DTV를 관리하기 위해 size를 알아야 하므로 generation number를 저장하기 위한 counter만을 사용하고, 그 이후의 항목은 TLS 변수의 실제 위치를 가리키기 때문에 구조체가 아니라 공용체를 사용하고 있다.
1 2 3 4 5 6 7 8 | ∕* Type for the dtv. *∕ typedef union dtv{ size_t counter; struct { void *val; bool is_static; } pointer; } dtv_t; | cs |
- counter : DTV 0번 항목(DTV를 관리하기 위한 generation number) 저장
- pointer.val : 이후의 항목(TLS의 시작 위치)은 여기에 저장
- pointer.is_static : 해당 라이브러리가 프로그램 시작 시 로드 되었는지(static), dlopen()과 같은 함수로 실행 시 동적으로 로드 되었는지(dynamic)
TLS의 시작 위치는 DTV에 저장되어 있고, DTV의 위치는 TCB가 알고 있다. TCB를 찾아가기 위해서는 TCB의 위치를 알고 있는 무언가가 필요하다. 그래서 x86의 경우는 TLS용 세그먼트 디스크립터를 만들어 gs 레지스터를 이용해 가리키고, 이를 따라가면 Base Address에 TCB가 저장되어 있다. 이를 위해 커널에는 2가지 System Call을 사용한다.
System Call
- set_thread_area() : 세그먼트 디스크립터의 Base_addr 필드에 TCB의 시작 위치를 설정하는 함수
- get_thread_area() : 세그먼트 디스크립터의 Base_addr 필드에 저장된 TCB의 시작 위치를 가져오는 함수
set_thread_area() System Call을 호출하여 세그먼트 디스크립터를 설정하기 전에 준비를 해야할 필요가 있다. 그래서 동적 링커는 프로그램을 실행할 때 실행 파일과 의존 라이브러리를 전부 로드해서 ELF Format의 프로그램 헤더의 PT_TLS를 확인해 TLS 영역을 포함하는지 검사하여 정보 저장한다. 그리고 TLS 정보를 이용해 TCB와 DTV를 구성하고, TLS 영역을 초기화 이미지로 채워, set_thread_area() System Call을 호출한다.
참고
- http://studyfoss.egloos.com/5259841 : 굉장히 자세함, 이 글의 모든 내용은 여기서 나옴
- http://softwaretechnique.jp/Linux/SystemCall/sc_01.html : 일본어, 마지막 부분의 TLS 도식화와 gs 세그먼트 설명이 좋음
- http://blog.talosintelligence.com/2016/01/bypassing-miniupnp-stack-smashing.html : 바로 위꺼랑 같이 보면 좋을 듯
- http://www.it610.com/article/5012031.htm : 중국어, 전체 도식화 과정에서 많은 도움이 됨
'System > Linux' 카테고리의 다른 글
Linux Crash Monitor[dmesg] (0) | 2018.01.23 |
---|---|
Reverse Shellcode (0) | 2017.08.12 |
Linux Device Driver 정리 (2) (0) | 2017.06.01 |
Linux Device Driver 정리 (1) (0) | 2017.05.28 |
Linux 커널 및 모듈 공부 기초 (0) | 2017.05.27 |