Egloos | Log-in
F/OSS study
F/OSS study
[ELF] TLS (Thread Local Storage) (1)
arch: x86
linux: 2.6.32
gcc: 4.4.1
glibc: 2.10.1


TLS란 각 스레드 별로 다른 값을 가지는 전역 변수를 말한다.
스택에 잡히는 지역 변수는 스레드마다 별도의 스택을 사용하므로 당연히 다른 값을 가지지만
(일반) 전역 변수의 경우에는 모든 스레드가 공유하므로 접근 시 race condition이 생길 수 있다.
따라서 스레드마다 개별적으로 사용할 수 있는(thread-local) 변수를 사용하여 안정성 및 성능을 높일 수 있다.

사실 자료구조를 스레드 수 만큼 할당하고 특정 스레드는 정해진 위치에만 접근하도록 하면
동일한 효과를 얻을 수 있을테지만 이에 따르는 모든 책임은 프로그래머가 감수해야 할 것이다.

전통적으로 이러한 TLS의 개념은
POSIX thread가 제공하는 TSD (Thread-Speicific Data) 관련 API를 통해 구현되었다.
하지만 이는 프로그래머가 직접 key를 생성/관리해야 하며
(보다 중요한 문제는) void * 타입의 데이터만 저장이 가능하므로
별도의 메모리 공간을 동적 할당한 뒤에 해당 포인터를 저장하는 방식으로 사용하기 때문에
메모리 누수 문제를 발생시킬 수 있는 가능성이 다분하다.

이러한 문제를 간단히 해결할 수 있는 기법으로 도입된 것이 바로 TLS이다.
TLS를 이용하면 컴파일러 및 링커가 복잡한 처리를 도맡아 주기 때문에
프로그래머는 일반적인 변수를 이용하는 방식과 동일하게 이용할 수 있다.

오직 필요한 것은 변수 선언 시 다음과 같이 (GNU 확장) __thread 키워드를 써서
이 변수가 TLS에 저장됨을 알려주는 일이다.

__thread int x;

ELF 형식은 TLS를 지원하기 위해 많은 변경이 이루어졌다.
먼저 TLS 변수를 저장하기 위해 (기존의 .data/.bss에 대응하는) .tdata/.tbss 섹션이 추가되었고
이들은 이후 링크 과정에서 PT_TLS 타입의 데이터 세그먼트로 합쳐지게 된다.
이렇게 만들어진 데이터 세그먼트는 초기화 이미지(initialization image)라고 하며
직접 사용되지는 않고 각 스레드 생성 시 TLS 영역을 초기화하기 위해 사용된다.
(즉, 모든 스레드에 대해 TLS 변수들의 초기값은 동일하다.)

그럼 어떻게 스레드 별로 이러한 TLS 영역을 자동으로 접근할 수 있을까?
먼저 CPU에 특정한 레지스터가 있어서 현재 실행 중인 스레드의 TCB(Thread Control Block)를
항상 가리키고 있다고 가정해 보자. (당연히 이 TCB는 각 스레드 별로 따로 존재하는 자료 구조이다.)
TCB 내에는 로드된 각 모듈(실행 파일 및 공유 라이브러리)의 TLS 영역의 시작 주소를 가지고 있는
DTV (Dynamic Thread Vector)의 포인터를 가지고 있다.

DTV의 0번 항목은 DTV 자체를 관리하기 위한 generation number를 저장하고 있으며
그 이후의 항목들은 프로그램이 로드한 각 라이브러리에 포함된 TLS 시작 위치(offset)를 저장한다.
실행 파일 자체에 TLS 영역이 포함되어 있다면 이에 대한 정보는 1번 항목에 저장되며
나머지는 동적 링커가 로드한 순서대로 저장된다.glibc에서 DTV는 다음과 같이 정의되어 있다.

∕* Type for the dtv.  *∕
typedef union dtv{
  size_t counter;
  struct  {
    void *val;
    bool is_static;
  } pointer;
} dtv_t;

0번 항목은 counter 필드를 이용하여 값을 저장하고
이후의 항목들은 pointer.val 필드를 이용하여 값을 저장한다. (union임에 주의!)
pointer.is_static은 해당 라이브러리가 프로그램 시작 시 로드되었는지(static)
아니면 dlopen()을 이용하여 실행 시 동적으로 로드되었는지(dynamic)를 나타내기 위한 것이다.

동적 링커는 프로그램을 실행할 때 해당 실행 파일 자체는 물론이고
의존하는 모든 라이브러리를 로드하여 TLS 영역을 포함하는지 검사하여 정보를 저장한다.
(위에서 말했듯이 프로그램 헤더에서 PT_TLS 타입의 항목이 있는지 검사하면 된다.)

이제 동적 링커는 로드된 모든 모듈에 대한 TLS 정보를 가지고 있으므로
이들 각각의 크기를 합하여 전체 TLS 영역의 크기를 결정하고
각 모듈에 대한 시작 offset을 계산할 수 있다.

이렇게 프로그램 시작 시 로드된 (static) 모듈의 TLS 영역은 TCB와 같은 영역에 저장되는데
x86의 경우 TCB의 크기가 가변적이기 때문에 TLS 영역이 TCB의 (바로) 앞에 위치한다.
이를 그림으로 나타내면 다음과 같다.

위에서 TCB의 시작 위치를 가리키고 있는 tp는 우리가 앞서 가정한 스레드 레지스터이다.
(참고로 아래 첨자 t는 임의의 스레드를 지칭하는 것이므로 무시해도 된다.)
CPU 레지스터가 충분한 아키텍처의 경우 레지스터 중 하나를 할당하여 tp로 이용할 수 있겠지만
x86의 경우에는 그렇지가 못하기 때문에 다른 방법이 필요하다.

그것은 바로 세그먼트를 이용하는 방법인데
일반적으로 사용되는 user/kernel code/data 세그먼트 디스크팁터 외에
TLS 전용의 세그먼트 디스크립터를 만들어서 gs 레지스터가 이를 가리키는 방식을 이용한다.
해당 세그먼트의 시작 위치(offset 0)에는 TCB가 저장되도록 한다.

예전에는 (리눅스 커널에서 TLS를 지원하지 않았을 때) 각 스레드 별로 LDT를 할당하여 관리하는 방식을 이용했지만
이제는 리눅스 커널에서 GDT의 6번 항목을 이러한 용도로 배정하였기 때문에 LDT를 이용할 필요가 없다.
참고로 각 스레드마다 LDT 항목을 하나씩 배정하게되면 세그먼트 레지스터의 크기 제한으로 인해
최대 8192 개의 스레드 밖에(?!) 생성할 수 없는 문제가 있었다.

이를 위해 커널에서 set/get_thread_area()라는 새로운 시스템 콜을 제공하게 되었다.
이 시스템 콜은 인자로 주어진 디스크립터 정보를 이용하여 TLS 영역의 정보를 구성하고
매 컨텍스트 스위칭 시에 해당 디스크립터 정보를 GDT의 해당 위치에 기록한다.

사실 커널에서는 이러한 용도로 최대 3개의 세그먼트 디스크립터를 이용할 수 있도록 예약해 두었는데
이 중 첫 번째인 6번은 glibc가, 두 번째인 7번은 wine이 사용한다고 하며 8번은 나중을 위해 남겨둔 것 같다.

동적 링커는 위에서 얻은 TLS 정보를 이용해 TCB 및 DTV를 구성하고
TLS 영역을 초기화 이미지의 값을 복사하여 채운 뒤 set_thread_area() 시스템 콜을 호출한다.
디스크립터의 base_addr 필드는 TCB의 시작 위치를 가리키도록 설정된다.
아래는 glibc에 정의된 TCB의 헤더 부분이다.
(실제 TCB는 이를 포함하여 더욱 많은 정보를 포함하지만 지금은 신경쓸 필요가 없다.)

typedef struct {
  void *tcb;        ∕* Pointer to the TCB.  Not necessarily the
                       thread des-riptor used by libpthread.  *∕
  dtv_t *dtv;
  void *self;        ∕* Pointer to the thread des-riptor.  *∕
  int multiple_threads;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  int gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
  int private_futex;
#else
  int __unused1;
#endif
  ∕* Reservation of some values for the TM ABI.  *∕
  void *__private_tm[5];
} tcbhead_t;

지금 우리가 관심있는 부분은 dtv에 대한 부분이다.
이제 다음과 같은 예제를 이용하여 위의 내용을 확인해 보도록 하겠다.
먼저 위와 같은 데이터 타입을 정의하기 위한 헤더 파일을 만들어 둔다.

tls.h:
#include <sys/types.h>

typedef union dtv {
  size_t counter;
  struct  {
    void *val;
    int is_static;
  } pointer;
} dtv_t;

typedef struct {
  void *tcb;
  dtv_t *dtv;
} tcbhead_t;

이제 다음과 같은 예제 파일을 작성한다.

main.c:
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <asm/ldt.h>
#include <sys/syscall.h>
#include "tls.h"

__thread int tls_a;
__thread int tls_b = 0xdeadbeef;

void print_tp(void)
{
  unsigned short tp;
  unsigned long tp0;

  asm volatile("movw %%gs, %0" : "=r" (tp));
  printf("gs: index = %u, ti = %u, dpl = %u\n",
          tp >> 3, !!(tp & 4), tp & 3);
  asm volatile("movl %%gs:0, %0" : "=r" (tp0));
  printf("address of TCB (%%gs:0)    = %#010lx\n", tp0);
}

void print_tls(void)
{
  int i = 0;
  dtv_t *dtv;
  tcbhead_t *tcb;
  struct user_desc u_info = { .entry_number = 6 };

  syscall(SYS_get_thread_area, &u_info);
  tcb = (tcbhead_t *) u_info.base_addr;
  dtv = tcb->dtv;

  printf("get thread area  base addr: %#010x\n", u_info.base_addr);
  printf("dynamic thread vector addr: %p\n", dtv);
  printf("DTV: generation number = %d\n", dtv[0].counter);

  for (i = 1; dtv[i].pointer.val; i++)
    printf("DTV: [%d] = %p (%s)\n", i, dtv[i].pointer.val,
            dtv[i].pointer.is_static ? "static" : "dynamic");
  printf("tls_a ptr = %p, value = %x\n", &tls_a, tls_a);
  printf("tls_b ptr = %p, value = %x\n", &tls_b, tls_b);
  printf("errno ptr = %p, value = %x\n", &errno, errno);
}

int main(void)
{
  print_tp();
  print_tls();
  return 0;
}

먼저 print_tp() 함수는 스레드 레지스터의 정보를 출력하는 함수이다.
gs 레지스터를 읽어서 그 내용을 분석한 것인데 위의 설명이 맞다면
GDT 6번 항목을 나타내기 위해ti: 0, index: 6 값을 가져야 할 것이다.
또한 스레드 레지스터는 TCB의 주소를 가리키므로
해당 세그먼트의 offset 0에는 이 주소값이 저장되어 있을 것이다.

print_tls() 함수는 get_thread_area() 시스템 콜을 호출하여 TCB 정보를 얻고
이를 통해 DTV 정보를 살펴본 뒤 각 변수의 주소와 비교해 본 것이다.
이제 컴파일 한 뒤에 실행해 보면 아래와 같은 결과를 얻을 수 있다.

$ gcc main.c
$ ./a.out
gs: index = 6, ti = 0, dpl = 3
address of TCB (%gs:0)    : 0xb76e76d0
get thread area  base addr: 0xb76e76d0
dynamic thread vector addr: 0xb76e7b68
DTV: generation number = 1
DTV: [1] = 0xb76e76c8 (static)
DTV: [2] = 0xb76e7688 (static)
tls_a ptr = 0xb76e76cc, value = 0
tls_b ptr = 0xb76e76c8, value = deadbeef
errno ptr = 0xb76e7690, value = 0

먼저 tp, 즉 gs 레지스터의 값은 예상대로 나왔음을 확인할 수 있다.
tp를 통해 접근한 값과 get_thread_area() 시스템 콜을 통해 얻은 주소도 동일하다.
tls_a와 tls_b 변수는 실행 파일 내의 TLS 영역에 저장된 변수이다.
DTV 1번 항목이 이에 대한 시작 위치를 포함하고 있는 것을 볼 수 있다.
errno 변수는 glibc 내의 TLS 영역에 저장된 변수이다.
마찬가지로 DTV 2번 항목이 이에 대한 시작 위치를 포함하고 있다.

이제 새로운 스레드를 생성한 후에 동일한 함수를 수행하여 어떤 결과가 나오는지 살펴보기로 하자.
참고로 스레드 생성 시 clone() 시스템 콜에 CLONE_SETTLS 플래그를 설정하면
새로운 스레드를 위한 TLS 정보를 바로 설정할 수 있다. (glibc/nptl에서도 물론 이 방법을 이용한다.)
<pthread.h> 헤더 파일을 추가로 #include 한 뒤에 main() 함수 부분을 다음과 같이 변경한다.

void *thread_print_tls(void *arg)
{
  print_tp();
  print_tls();
  return arg;
}

int main(void)
{
  void *result;
  pthread_t pth;

  print_tp();
  print_tls();
  puts("==========================================");

  pthread_create(&pth, NULL, thread_print_tls, NULL);
  pthread_join(pth, &result);
  return 0;
}

실행 결과는 다음과 같다.

$ gcc main.c -pthread
$ ./a.out
gs: index = 6, ti = 0, dpl = 3
address of TCB (%gs:0)    : 0xb76196d0
get thread area  base addr: 0xb76196d0
dynamic thread vector addr: 0xb7619b68
DTV: generation number = 1
DTV: [1] = 0xb76196c8 (static)
DTV: [2] = 0xb7619688 (static)
tls_a ptr = 0xb76196cc, value = 0
tls_b ptr = 0xb76196c8, value = deadbeef
errno ptr = 0xb7619690, value = 0
==========================================
gs: index = 6, ti = 0, dpl = 3
address of TCB (%gs:0)    : 0xb7618b70
get thread area  base addr: 0xb7618b70
dynamic thread vector addr: 0x9ab3010
DTV: generation number = 1
DTV: [1] = 0xb7618b68 (static)
DTV: [2] = 0xb7618b28 (static)
tls_a ptr = 0xb7618b6c, value = 0
tls_b ptr = 0xb7618b68, value = deadbeef
errno ptr = 0xb7618b30, value = 0

위에서 보듯이 메인 스레드와 새로 생성된 스레드는 서로 별개의 TCB, DTV, TLS 영역을 가지고 있지만
TLS 변수의 초기값은 동일하다.

=== 참고 문헌 ===
by namhyung | 2010/02/28 17:38 | System | 트랙백(1) | 덧글(4)
트랙백 주소 : http://studyfoss.egloos.com/tb/5259841
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Tracked from F/OSS study at 2010/03/05 23:37

제목 : [ELF] TLS (Thread Local Stor..
arch: x86 linux: 2.6.32 gcc: 4.4.1 glibc: 2.10.1 이전 글 보기: [ELF] TLS (Thread Local Storage) (1) 이제 libdl을 이용하여 라이브러리를 동적으로 로드하는 경우를 살펴보자. 이 경우에는 프로그램 시작 시 동적 링커가 알지 못하는 전혀 새로운 TLS 영역이 추가되어야 한다. 따라서 이러한 영역은 정적 할당이 불가능해지므로 동적 할당 기법이 사용된다. 동적 링커......more

Commented by eunsu at 2011/12/20 13:50
이런 좋은 글에 덧글이 없어 하나 올립니다.

좋은 자료 잘 보고 갑니다.

좋은 하루 보내세요.
Commented by namhyung at 2011/12/24 19:30
답글 남겨주셔서 감사합니다. 행복한 연말 보내세요.
Commented by 홍성빈 at 2014/04/28 11:34
TLS..!!
아~ 그런가보다 하고 쓰고 있었는데 이렇게 복잡한 거였군요 !!! 허
Commented by fdsaf at 2016/11/19 20:30
고급진 글 감사합니다.

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶

카테고리
General
Application
System
Kernel
Book
Tips
태그
compiler sed synchronization glibc x86 SMP awk documentation binutils patch linux CARM block-layer git memory blktrace perf emacs scm kernel C build CAaQA3 vcs computer-architecture algorithm bash gcc script elf
전체보기
이글루 파인더

최근 등록된 덧글
informsi yang bagus dan be..
by pordanaia at 08/05
Informsi yang bagus dan b..
by jamalsu at 08/01
as inforasi yang menarik http..
by jamalsu at 07/22
최근 등록된 트랙백
Tod's Ferrari Homme
by Tods Pas Cher,Kodak did ..
Mocassin Femme
by Mocassins Homme, I got so..
natural garcinia cambogia
by
rss

skin by jiinny


X