Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19542] ファイルの概要

このコミットは、Goランタイムのsrc/pkg/runtime/vdso_linux_amd64.cファイルに対する変更です。このファイルは、LinuxシステムにおけるvDSO (virtual Dynamic Shared Object) のシンボルをルックアップするためのGoランタイムの内部実装を扱っています。vDSOは、ユーザー空間のアプリケーションがシステムコールを直接呼び出すことなく、カーネルの機能に高速にアクセスできるようにするためのメカニズムです。具体的には、__vdso_time, __vdso_gettimeofday, __vdso_clock_gettimeといった時間関連の関数をvDSOから取得し、Goプログラム内で利用します。

コミット

commit 1db4c8dc413b588668851eddf05426dabb08c95a
Author: Ian Lance Taylor <iant@golang.org>
Date:   Fri Jun 13 13:29:26 2014 -0700

    runtime: fix VDSO lookup to use dynamic hash table
    
    Reportedly in the Linux 3.16 kernel the VDSO will not have
    section headers or a normal symbol table.
    
    Too late for 1.3 but perhaps for 1.3.1, if there is one.
    
    Fixes #8197.
    
    LGTM=rsc
    R=golang-codereviews, mattn.jp, rsc
    CC=golang-codereviews
    https://golang.org/cl/101260044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1db4c8dc413b588668851eddf05426dabb08c95a

元コミット内容

runtime: fix VDSO lookup to use dynamic hash table

Reportedly in the Linux 3.16 kernel the VDSO will not have
section headers or a normal symbol table.

Too late for 1.3 but perhaps for 1.3.1, if there is one.

Fixes #8197.

LGTM=rsc
R=golang-codereviews, mattn.jp, rsc
CC=golang-codereviews
https://golang.org/cl/101260044

変更の背景

このコミットの主な背景は、Linuxカーネルのバージョン3.16でvDSO (virtual Dynamic Shared Object) の構造が変更されるという情報にあります。従来のvDSOは、ELF (Executable and Linkable Format) 形式のセクションヘッダや通常のシンボルテーブルを持っていましたが、Linux 3.16カーネルではこれらの情報が提供されなくなることが報告されました。

Goランタイムは、__vdso_time__vdso_gettimeofday__vdso_clock_gettimeといった特定の関数をvDSOから動的にルックアップして利用しています。これらの関数は、システムコールを介さずに高速に時刻情報を取得するために重要です。vDSOの構造変更により、従来のシンボルルックアップ方法が機能しなくなるため、Goランタイムがこれらの関数を見つけられなくなり、結果としてGoプログラムの動作に影響が出る可能性がありました。

この問題を解決するため、Goランタイムは、セクションヘッダや通常のシンボルテーブルに依存しない、ELFのダイナミックリンキング仕様で定義されている「ダイナミックハッシュテーブル」を利用したシンボルルックアップ方式に移行する必要がありました。この変更は、Go 1.3のリリースには間に合わなかったものの、将来のバージョン(おそらく1.3.1)での対応が検討されました。

前提知識の解説

vDSO (virtual Dynamic Shared Object)

vDSOは、Linuxカーネルがユーザー空間のプロセスに提供する特殊な共有ライブラリです。これは物理的なファイルとしてディスク上に存在するのではなく、カーネルが各プロセスの仮想アドレス空間にマッピングします。vDSOの目的は、gettimeofdayclock_gettimeのような頻繁に呼び出されるシステムコールを、実際のシステムコールトラップ(カーネルモードへの切り替え)なしに、ユーザー空間で直接実行できるようにすることです。これにより、システムコールのオーバーヘッドが削減され、パフォーマンスが向上します。vDSOはELF形式で提供されます。

ELF (Executable and Linkable Format)

ELFは、Unix系システムで実行可能ファイル、オブジェクトコード、共有ライブラリ、コアダンプなどを格納するために使用される標準ファイル形式です。ELFファイルは、ヘッダ、プログラムヘッダテーブル、セクションヘッダテーブル、および様々なセクション(コード、データ、シンボルテーブルなど)で構成されます。

ダイナミックリンキング (Dynamic Linking)

ダイナミックリンキングは、プログラムの実行時に共有ライブラリ(例: .soファイルやvDSO)をロードし、リンクするプロセスです。これにより、ディスクスペースの節約、メモリ使用量の削減、およびライブラリの更新が容易になるという利点があります。ダイナミックリンカーは、プログラムが必要とするシンボル(関数や変数)を共有ライブラリ内で見つけ出し、それらのアドレスを解決します。

シンボルテーブル (Symbol Table)

シンボルテーブルは、プログラム内の関数や変数などのシンボル名と、それらのアドレスや型などの関連情報をマッピングするデータ構造です。ELFファイルには、静的リンキング用のシンボルテーブル(.symtabセクション)と、ダイナミックリンキング用のシンボルテーブル(.dynsymセクション)が存在します。

ELFハッシュテーブル (ELF Hash Table)

ELFハッシュテーブルは、ダイナミックリンキングにおいてシンボルを効率的にルックアップするために使用されるデータ構造です。これは、シンボル名からシンボルテーブル内のエントリを高速に検索するために設計されています。従来のシンボルテーブルを線形探索するよりもはるかに高速です。ELFハッシュテーブルは、DT_HASHエントリによって指し示され、通常はバケットとチェーンの配列で構成されます。

  • バケット (Bucket): ハッシュ値に基づいてシンボルをグループ化するための配列。各バケットは、同じハッシュ値を持つシンボルのチェーンの先頭を指します。
  • チェーン (Chain): 同じハッシュ値を持つシンボルを連結リストのように繋ぐための配列。

関連するELF構造体と定数

  • AT_SYSINFO_EHDR (Auxiliary Vector): プログラムが起動する際にカーネルから渡される補助ベクタ(Auxiliary Vector)の一つ。vDSOのELFヘッダのアドレスを指します。Goランタイムはこれを利用してvDSOのベースアドレスを取得します。
  • PT_LOAD (Program Header Type): プログラムヘッダテーブルのエントリタイプの一つで、メモリにロードされるセグメントを示します。vDSOのロードアドレスとオフセットを計算するために使用されます。
  • PT_DYNAMIC (Program Header Type): プログラムヘッダテーブルのエントリタイプの一つで、ダイナミックリンキング情報を含むセグメントを示します。このセグメント内にDT_HASHなどのエントリが含まれます。
  • DT_NULL (Dynamic Tag): ダイナミックセクションの終端を示すタグ。
  • DT_HASH (Dynamic Tag): ダイナミックシンボルハッシュテーブルのアドレスを示すタグ。
  • DT_STRTAB (Dynamic Tag): 文字列テーブルのアドレスを示すタグ。シンボル名はこの文字列テーブルに格納されます。
  • DT_SYMTAB (Dynamic Tag): シンボルテーブルのアドレスを示すタグ。
  • DT_VERSYM (Dynamic Tag): バージョンシンボルテーブルのアドレスを示すタグ。シンボルのバージョン情報を提供します。
  • STT_FUNC (Symbol Type): シンボルが関数であることを示します。
  • STB_GLOBAL (Symbol Binding): シンボルがグローバル結合を持つことを示します。
  • STB_WEAK (Symbol Binding): シンボルが弱い結合を持つことを示します。
  • SHN_UNDEF (Special Section Index): シンボルが未定義であることを示します。

技術的詳細

このコミットは、GoランタイムがLinux vDSOからシンボルをルックアップする方法を、従来のセクションヘッダベースのシンボルテーブル検索から、ELFダイナミックリンキング仕様に基づくハッシュテーブル検索に切り替えるものです。

変更点概要

  1. vdso_info構造体の変更:

    • num_symフィールドが削除されました。これは、セクションヘッダからシンボル数を取得する必要がなくなったためです。
    • ハッシュテーブル関連のフィールドが追加されました: bucket, chain, nbucket, nchain。これらはELFハッシュテーブルの構造を反映しています。
  2. vdso_init_from_sysinfo_ehdr関数の変更:

    • セクションヘッダ(Elf64_Shdr)を解析するロジックが削除されました。特にSHT_DYNSYMセクションからシンボル数を取得する部分がなくなりました。
    • ダイナミックセクション(PT_DYNAMICセグメント)の解析において、DT_HASHタグを処理するロジックが追加されました。これにより、ハッシュテーブルのアドレスが取得されます。
    • ハッシュテーブルのヘッダ(hash[0]nbuckethash[1]nchain)を解析し、bucketchain配列へのポインタを設定するロジックが追加されました。
    • シンボル文字列テーブル (symstrings)、シンボルテーブル (symtab)、およびハッシュテーブル (hash) の全てが取得できた場合にのみvdso_info->validtrueに設定するようになりました。
  3. vdso_parse_symbols関数の変更:

    • シンボルを線形探索する従来のループ(for(i=0; i<vdso_info->num_sym; i++))が削除されました。
    • 代わりに、ELFハッシュテーブルを利用したシンボルルックアップロジックが実装されました。これは、シンボル名に対応するハッシュ値を計算し、そのハッシュ値が指すバケットからチェーンを辿ってシンボルを検索するものです。
    • symbol_key構造体にsym_hashフィールドが追加され、事前に計算されたシンボル名のハッシュ値が格納されるようになりました。これにより、ルックアップ時にハッシュ値を再計算する必要がなくなります。
    • シンボルバージョンチェックの際に、vdso_info->versym[i]からvdso_info->versym[chain]に変更され、ハッシュテーブルのチェーンインデックスを使用するように修正されました。

新しいシンボルルックアップのフロー

  1. vDSOの初期化 (vdso_init_from_sysinfo_ehdr):

    • カーネルから提供されるAT_SYSINFO_EHDR補助ベクタからvDSOのELFヘッダのアドレスを取得します。
    • プログラムヘッダテーブルを走査し、PT_LOADセグメントからvDSOのロードオフセットを計算します。
    • PT_DYNAMICセグメントを見つけ、その中のダイナミックタグを解析します。
    • DT_STRTABから文字列テーブルのアドレスを、DT_SYMTABからシンボルテーブルのアドレスを、そして**DT_HASHからハッシュテーブルのアドレス**を取得します。
    • 取得したハッシュテーブルの先頭2つのワードから、バケット数 (nbucket) とチェーン数 (nchain) を読み取ります。
    • ハッシュテーブルのアドレスから、bucket配列とchain配列へのポインタを設定します。
  2. シンボルの解析 (vdso_parse_symbols):

    • Goランタイムがルックアップしたい各シンボル(例: __vdso_time)について、事前に計算されたハッシュ値(sym_hash)を使用します。
    • sym_hashnbucketで割った剰余をインデックスとして、bucket配列からチェーンの先頭インデックスを取得します。
    • そのチェーンをchain配列を使って辿ります。各チェーンエントリはシンボルテーブル内のインデックスを指します。
    • チェーン上の各シンボルについて、以下のチェックを行います。
      • シンボルタイプが関数 (STT_FUNC) であること。
      • シンボル結合がグローバル (STB_GLOBAL) または弱い (STB_WEAK) であること。
      • シンボルが未定義 (SHN_UNDEF) でないこと。
      • シンボル名が目的のシンボル名と一致すること(runtime·strcmpを使用)。
      • バージョン情報がある場合、シンボルバージョンが一致すること。
    • 全ての条件を満たすシンボルが見つかった場合、そのシンボルのアドレスを計算し、対応するGoランタイムの変数ポインタ(例: runtime·__vdso_time_sym)に格納します。

この変更により、Linuxカーネル3.16以降でvDSOのセクションヘッダや通常のシンボルテーブルが利用できなくなった場合でも、GoランタイムがvDSO関数を正しくルックアップできるようになります。

コアとなるコードの変更箇所

src/pkg/runtime/vdso_linux_amd64.c

  1. vdso_info 構造体の変更:

    --- a/src/pkg/runtime/vdso_linux_amd64.c
    +++ b/src/pkg/runtime/vdso_linux_amd64.c
    @@ -148,9 +161,10 @@ struct vdso_info {
     	uintptr load_offset;  /* load_addr - recorded vaddr */
     
     	/* Symbol table */
    -	int32 num_sym;
     	Elf64_Sym *symtab;
     	const byte *symstrings;
    +	Elf64_Word *bucket, *chain;
    +	Elf64_Word nbucket, nchain;
     
     	/* Version table */
     	Elf64_Versym *versym;
    
  2. symbol_key 構造体と sym_keys 配列の変更:

    --- a/src/pkg/runtime/vdso_linux_amd64.c
    +++ b/src/pkg/runtime/vdso_linux_amd64.c
    @@ -132,6 +144,7 @@ typedef struct
     
     typedef struct {
     	byte* name;
    +	int32 sym_hash;
     	void** var_ptr;
     } symbol_key;
     
    @@ -148,9 +161,10 @@ struct vdso_info {
     
     #define SYM_KEYS_COUNT 3
     static symbol_key sym_keys[] = {
    -	{ (byte*)"__vdso_time", &runtime·__vdso_time_sym },
    -	{ (byte*)"__vdso_gettimeofday", &runtime·__vdso_gettimeofday_sym },
    -	{ (byte*)"__vdso_clock_gettime", &runtime·__vdso_clock_gettime_sym },
    +	{ (byte*)"__vdso_time", 0xa33c485, &runtime·__vdso_time_sym },
    +	{ (byte*)"__vdso_gettimeofday", 0x315ca59, &runtime·__vdso_gettimeofday_sym },
    +	{ (byte*)"__vdso_clock_gettime", 0xd35ec75, &runtime·__vdso_clock_gettime_sym },
     };
    
  3. vdso_init_from_sysinfo_ehdr 関数の変更:

    • セクションヘッダ関連のコード削除。
    • DT_HASHの処理追加。
    • ハッシュテーブルのnbucketnchainの読み込みとbucket/chainポインタの設定。
    --- a/src/pkg/runtime/vdso_linux_amd64.c
    +++ b/src/pkg/runtime/vdso_linux_amd64.c
    @@ -176,18 +190,15 @@ vdso_init_from_sysinfo_ehdr(struct vdso_info *vdso_info, Elf64_Ehdr* hdr)
     {
     	uint64 i;
     	bool found_vaddr = false;
    +	Elf64_Phdr *pt;\n+	Elf64_Dyn *dyn;\n+	Elf64_Word *hash;
     
    +	vdso_info->valid = false;
     	vdso_info->load_addr = (uintptr) hdr;
     
    -	Elf64_Phdr *pt = (Elf64_Phdr*)(vdso_info->load_addr + hdr->e_phoff);
    -	Elf64_Shdr *sh = (Elf64_Shdr*)(vdso_info->load_addr + hdr->e_shoff);
    -	Elf64_Dyn *dyn = 0;
    -
    -	for(i=0; i<hdr->e_shnum; i++) {
    -		if(sh[i].sh_type == SHT_DYNSYM) {
    -			vdso_info->num_sym = sh[i].sh_size / sizeof(Elf64_Sym);
    -		}
    -	}
    +	pt = (Elf64_Phdr*)(vdso_info->load_addr + hdr->e_phoff);
    +	dyn = nil;
     
     	// We need two things from the segment table: the load offset
     	// and the dynamic table.
    @@ -206,6 +217,11 @@ vdso_init_from_sysinfo_ehdr(struct vdso_info *vdso_info, Elf64_Ehdr* hdr)
     	return;  // Failed
     
     	// Fish out the useful bits of the dynamic table.
    +	hash = nil;
    +	vdso_info->symstrings = nil;
    +	vdso_info->symtab = nil;
    +	vdso_info->versym = nil;
    +	vdso_info->verdef = nil;
     	for(i=0; dyn[i].d_tag!=DT_NULL; i++) {
     		switch(dyn[i].d_tag) {
     		case DT_STRTAB:
    @@ -218,6 +234,11 @@ vdso_init_from_sysinfo_ehdr(struct vdso_info *vdso_info, Elf64_Ehdr* hdr)
     			((uintptr)dyn[i].d_un.d_ptr
     			 + vdso_info->load_offset);
     			break;
    +		case DT_HASH:
    +			hash = (Elf64_Word *)
    +			  ((uintptr)dyn[i].d_un.d_ptr
    +			   + vdso_info->load_offset);
    +			break;
     		case DT_VERSYM:
     			vdso_info->versym = (Elf64_Versym *)
     				((uintptr)dyn[i].d_un.d_ptr
    @@ -230,12 +251,18 @@ vdso_init_from_sysinfo_ehdr(struct vdso_info *vdso_info, Elf64_Ehdr* hdr)
     		break;
     		}
     	}
    -	if(vdso_info->symstrings == nil || vdso_info->symtab == nil)
    +	if(vdso_info->symstrings == nil || vdso_info->symtab == nil || hash == nil)
     		return;  // Failed
     
     	if(vdso_info->verdef == nil)
     		vdso_info->versym = 0;
     
    +	// Parse the hash table header.
    +	vdso_info->nbucket = hash[0];
    +	vdso_info->nchain = hash[1];
    +	vdso_info->bucket = &hash[2];
    +	vdso_info->chain = &hash[vdso_info->nbucket + 2];
    +
     	// That\'s all we need.
     	vdso_info->valid = true;
     }
    
  4. vdso_parse_symbols 関数の変更:

    • シンボルルックアップロジックをハッシュテーブルベースに変更。
    • sym_keys[i].sym_hashを使用。
    • vdso_info->bucketvdso_info->chainを使用。
    --- a/src/pkg/runtime/vdso_linux_amd64.c
    +++ b/src/pkg/runtime/vdso_linux_amd64.c
    @@ -261,39 +288,41 @@ vdso_find_version(struct vdso_info *vdso_info, version_key* ver)
     	return -1; // can not match any version
     }
     
     static void
     vdso_parse_symbols(struct vdso_info *vdso_info, int32 version)
     {
    -	int32 i, j;
    +	int32 i;
    +	Elf64_Word chain;
    +	Elf64_Sym *sym;
     
     	if(vdso_info->valid == false)
     		return;
     
    -	for(i=0; i<vdso_info->num_sym; i++) {
    -		Elf64_Sym *sym = &vdso_info->symtab[i];
    +	for(i=0; i<SYM_KEYS_COUNT; i++) {
    +		for(chain = vdso_info->bucket[sym_keys[i].sym_hash % vdso_info->nbucket];
    +			chain != 0; chain = vdso_info->chain[chain]) {
     
    -		// Check for a defined global or weak function w/ right name.
    -		if(ELF64_ST_TYPE(sym->st_info) != STT_FUNC)
    -			continue;
    -		if(ELF64_ST_BIND(sym->st_info) != STB_GLOBAL &&
    -			ELF64_ST_BIND(sym->st_info) != STB_WEAK)
    -			continue;
    -		if(sym->st_shndx == SHN_UNDEF)
    -			continue;
    -
    -		for(j=0; j<SYM_KEYS_COUNT; j++) {
    -			if(runtime·strcmp(sym_keys[j].name, vdso_info->symstrings + sym->st_name) != 0)
    +			sym = &vdso_info->symtab[chain];
    +			if(ELF64_ST_TYPE(sym->st_info) != STT_FUNC)
    +				continue;
    +			if(ELF64_ST_BIND(sym->st_info) != STB_GLOBAL &&
    +				 Elf64_ST_BIND(sym->st_info) != STB_WEAK)
    +				continue;
    +			if(sym->st_shndx == SHN_UNDEF)
    +				continue;
    +			if(runtime·strcmp(sym_keys[i].name, vdso_info->symstrings + sym->st_name) != 0)
     				continue;
     
     			// Check symbol version.
     			if(vdso_info->versym != nil && version != 0
    -				&& vdso_info->versym[i] & 0x7fff != version)
    +				&& vdso_info->versym[chain] & 0x7fff != version)
     				continue;
     
    -			*sym_keys[j].var_ptr = (void *)(vdso_info->load_offset + sym->st_value);
    +			*sym_keys[i].var_ptr = (void *)(vdso_info->load_offset + sym->st_value);
    +			break;
     		}
     	}
     }
    

コアとなるコードの解説

vdso_info 構造体

この構造体は、vDSOに関する情報を保持します。変更前はnum_sym(シンボル数)を持っていましたが、これはセクションヘッダから取得される情報でした。変更後は、ELFハッシュテーブルの構造を直接反映するbucketchainnbucketnchainが追加されました。これにより、ハッシュテーブルベースのシンボルルックアップに必要な情報が全てこの構造体内に集約されます。

symbol_key 構造体と sym_keys 配列

symbol_key構造体は、ルックアップしたい各vDSO関数の名前と、その関数ポインタを格納するGoランタイム内の変数へのポインタを保持します。このコミットでsym_hashフィールドが追加されました。これは、各シンボル名に対応するELFハッシュ値(elf_hash関数で計算される)を事前に格納するためのものです。これにより、シンボルルックアップ時に毎回ハッシュ値を計算する手間が省かれ、効率が向上します。sym_keys配列には、__vdso_time, __vdso_gettimeofday, __vdso_clock_gettimeの3つの関数とそのハッシュ値、対応するGoランタイム変数が定義されています。

vdso_init_from_sysinfo_ehdr 関数

この関数は、vDSOのELFヘッダから必要な情報を初期化します。

  • セクションヘッダの解析削除: 従来のvDSOはセクションヘッダを持っていましたが、Linux 3.16以降ではこれがなくなるため、セクションヘッダを走査してSHT_DYNSYM(ダイナミックシンボルセクション)からシンボル数を取得するロジックが削除されました。
  • DT_HASHの処理追加: ダイナミックセクションを走査する際に、新たにDT_HASHタグを認識し、その値(ハッシュテーブルのアドレス)を取得するようになりました。
  • ハッシュテーブルヘッダの解析: 取得したハッシュテーブルのアドレスから、最初の2つのElf64_Wordを読み取り、それぞれnbucket(バケット数)とnchain(チェーン数)として設定します。その後、bucket配列とchain配列へのポインタを適切に設定します。これにより、ハッシュテーブルの構造がGoランタイムに認識されます。
  • 有効性チェックの強化: symstrings(文字列テーブル)、symtab(シンボルテーブル)に加えて、hash(ハッシュテーブル)も正常に取得できた場合にのみvdso_info->validtrueに設定するようになりました。これは、ハッシュテーブルがシンボルルックアップに必須となったためです。

vdso_parse_symbols 関数

この関数は、初期化されたvDSO情報を使って、目的のシンボル(関数)をvDSO内で検索し、そのアドレスをGoランタイムの対応する変数に設定します。

  • ハッシュテーブルベースの検索: 従来の線形探索に代わり、ELFハッシュテーブルを利用した効率的な検索ロジックが導入されました。
    1. ルックアップしたいシンボル(sym_keys[i])のsym_hash値を取得します。
    2. sym_hashvdso_info->nbucketで割った剰余を計算し、その結果をインデックスとしてvdso_info->bucket配列からチェーンの先頭インデックスを取得します。
    3. 取得したチェーンインデックスから、vdso_info->chain配列を辿って、同じハッシュ値を持つシンボルを順に検査します。
    4. 各シンボルについて、タイプ(関数であること)、結合(グローバルまたは弱いこと)、セクションインデックス(未定義でないこと)、そして名前の一致を確認します。
    5. バージョン情報がある場合は、vdso_info->versym[chain](チェーンインデックスに対応するバージョン情報)と目的のバージョンが一致するかを確認します。
  • アドレスの格納: 全ての条件を満たすシンボルが見つかった場合、そのシンボルのアドレス(vdso_info->load_offset + sym->st_value)を計算し、*sym_keys[i].var_ptrに格納します。これにより、GoプログラムからvDSO関数を呼び出せるようになります。

これらの変更により、GoランタイムはLinuxカーネルのvDSO構造の進化に対応し、将来にわたって安定してvDSO関数を利用できるようになりました。

関連リンク

参考にした情報源リンク