TLS (Thread Local Storage)

dandb3·2024년 4월 3일
0

pwnable

목록 보기
15/22

배경지식

여러 thread를 통해 multiprogramming이 가능하다.
이 때, 각 thread는 서로 다른 실행 흐름을 가져야 하므로 각자가 하나의 stack을 가지게 된다.
물론 전역변수 영역, heap 영역은 공유를 한다.
이 때, thread별로 내부적으로 전역변수가 사용되는 경우가 있는데, 이 때 내부적인 전역변수가 바로 TLS 공간에 저장되게 된다.

TLS?

이름 그대로, Thread가 Local하게 가지는 Storage라는 뜻이다.
이 포스트에서는 Linux / glibc, x86-64를 기준으로 다룬다.

TLS를 비롯하여 스레드와 관련된 여러 메타데이터들이 저장되어 있는 공간을 TCB(Thread Control Block)이라고 한다.
struct pthread가 TCB의 역할을 한다.

그리고 그 TCB에 접근할 때에는 fs 레지스터를 사용해서 접근한다. 즉, fs 레지스터가 TCB를 가리키게 된다.

접근 권한

userland에서는 fs의 값을 읽어오는 것 밖에는 할 수 없다.
fs의 값을 변경하는 것은 커널의 도움이 필요하다.
arch_prctl syscall을 통해서 fs/gs 세그먼트 레지스터의 값을 바꿀 수 있다.

좀 더 자세히는,,
MSR(Model Specific Register) 중 MSR_FS_BASE에 fs 레지스터가 가리키는 값이 저장된다.

TCB / TLS 초기화

main thread와 나머지 non-main thread의 TLS는 생성과정이 좀 다른데, 이는 사실 당연한 것으로 main thread는 커널에 의해 생성되지만, non-main thread는 유저영역에서 생성되기 때문이다.

기본적으로
init_tls -> dl_allocate_tls_storage(TCB, TLS와 관련된 메모리 할당) -> TLS_INIT_TP(arch_prctl을 통해 allocate한 메모리를 fs에 bind)의 과정을 통해 진행된다.

메모리 구조

tpttp_t는 thread 't'에 대한 thread register / pointer로, 실제 fs가 가리키는 곳을 의미한다.
tlsoffsetitlsoffset_i는 TLS block의 시작점을 가리킨다.

TLS block

알고 갈 정보 (정의?)

프로그램 시작 시 로드되는 dynamic library(..?)의 경우 생기는 TLS를 static TLS라고 부르고, 프로그램 시작 후에 dlopen으로 로드되는 dynamic library의 경우 dynamic TLS라고 부르자.

그림 상에서 TCB 바로 이전에 TLS block들이 여러 개 바로 붙어있는 것들을 확인할 수 있는데, 이 block들은 static TLS에 해당한다.
즉, 그림 상에서는 main 함수가 시작되기 이전에 3개의 dynamic loading이 이루어진 것이라고 볼 수 있다.

오른쪽 윗부분에는 TLS block이 따로 떨어져 있는데, 이들은 dlopen으로 runtime에 load된 모듈들의 것으로, dynamic TLS에 해당한다.

dtv

Dynamic Thread Vector의 약자이다.
TCB에서 dtv를 가리키는 포인터가 존재한다.
dtv에는 각 TLS의 시작점을 가리키는 포인터들이 존재한다.

그림으로 보았을 때, 총 5개의 module이 dynamically load 되어 있고, dtv는 각각의 module들의 TLS의 시작점을 가리키는 것을 확인할 수 있다.

gentgen_t의 경우, dtv의 0번째 entry이며, global generation counter로 load된 모듈의 수가 저장되어 있다.
그림 상으로는 나와있지 않지만, dtv의 -1번째 entry에는 dtv의 크기가 저장되어 있다.
dtv[-1], dtv[0]을 이용해서 dynamically load된 모듈의 수가 줄어들거나 늘어날 때에 알맞게 dtv의 크기를 조정해 주는 역할을 한다.

dtv[1] ~ 은 TLS의 시작점 주소가 저장된다.

dtv에 저장되는 struct는 다음과 같다.

typedef union dtv
{
  size_t counter;
  struct dtv_pointer pointer;
} dtv_t;

struct dtv_pointer
{
  void *val;
  void *to_free;
};

특히, main executable은 module 1로 고정된다.
즉, 그림에서도 확인할 수 있듯, module 1의 경우 TCB 바로 왼쪽에 위치해 있다.
그러므로 main executable에 정의되어 있는 TLS 내의 변수들은 fs레지스터의 값에서 offset을 빼서 바로 접근 가능하다.

물론, main executable이 아닌 이외의 module에서의 변수에 접근할 경우 dtv[ti_module].pointer + ti_offset의 형태로 일반적으로 접근할 수 있다.

사실 여기까지만 알아도 충분할 것 같지만..
조금 더 들어가 보자.

DTV와 static TLS의 초기화

dynamic linker : link_map을 통해 loaded module에 대한 정보를 가지고 있다.

초기화 순서
1. init_tls 호출 : link_map을 통해 dl_tls_dtv_slotinfo_list(TLS를 사용하는 module의 metadata를 포함하는 또 다른 global linked list) 의 정보를 채운다.
2. _dl_determine_tlsoffset 호출 : static TLS에서 각 module의 block이 어디에 위치할 것인지 결정한다.
3. _dl_allocate_tls_storage 호출 : static TLS, DTV 할당
...
n. _dl_allocate_tls_init 호출 : main thread의 static TLS, DTV를 초기화한다.

  • _dl_allocate_tls_init 함수
    dl_tls_max_dtv_idx보다 현재 dtv 크기가 작다면 resize해서 크기를 같게 만든다.
    현재 load된 모듈들을 순회하면서
    • dtv에 static TLS block 주소 할당
    • TLS에 data, bss영역의 값을 복사 후 data, bss영역의 값을 0으로 설정한다.

summary

  • FS register는 TCB를 가리킨다.
  • TLS 변수들의 offset은 compiler에 의해 결정되고, runtime에 바뀌지 않는다.
  • executable의 TLS block은 TCB 바로 전에 위치해 있다.

relocation...

뒷 내용은 ELF와 relocation관련 내용인데
복잡해서 다루지는 않을 듯..

참고 링크

https://chao-tic.github.io/blog/2018/12/25/tls#the-thread-register

profile
공부 내용 저장소

0개의 댓글