[インデックス 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の目的は、gettimeofday
やclock_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ダイナミックリンキング仕様に基づくハッシュテーブル検索に切り替えるものです。
変更点概要
-
vdso_info
構造体の変更:num_sym
フィールドが削除されました。これは、セクションヘッダからシンボル数を取得する必要がなくなったためです。- ハッシュテーブル関連のフィールドが追加されました:
bucket
,chain
,nbucket
,nchain
。これらはELFハッシュテーブルの構造を反映しています。
-
vdso_init_from_sysinfo_ehdr
関数の変更:- セクションヘッダ(
Elf64_Shdr
)を解析するロジックが削除されました。特にSHT_DYNSYM
セクションからシンボル数を取得する部分がなくなりました。 - ダイナミックセクション(
PT_DYNAMIC
セグメント)の解析において、DT_HASH
タグを処理するロジックが追加されました。これにより、ハッシュテーブルのアドレスが取得されます。 - ハッシュテーブルのヘッダ(
hash[0]
がnbucket
、hash[1]
がnchain
)を解析し、bucket
とchain
配列へのポインタを設定するロジックが追加されました。 - シンボル文字列テーブル (
symstrings
)、シンボルテーブル (symtab
)、およびハッシュテーブル (hash
) の全てが取得できた場合にのみvdso_info->valid
をtrue
に設定するようになりました。
- セクションヘッダ(
-
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]
に変更され、ハッシュテーブルのチェーンインデックスを使用するように修正されました。
- シンボルを線形探索する従来のループ(
新しいシンボルルックアップのフロー
-
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
配列へのポインタを設定します。
- カーネルから提供される
-
シンボルの解析 (
vdso_parse_symbols
):- Goランタイムがルックアップしたい各シンボル(例:
__vdso_time
)について、事前に計算されたハッシュ値(sym_hash
)を使用します。 sym_hash
をnbucket
で割った剰余をインデックスとして、bucket
配列からチェーンの先頭インデックスを取得します。- そのチェーンを
chain
配列を使って辿ります。各チェーンエントリはシンボルテーブル内のインデックスを指します。 - チェーン上の各シンボルについて、以下のチェックを行います。
- シンボルタイプが関数 (
STT_FUNC
) であること。 - シンボル結合がグローバル (
STB_GLOBAL
) または弱い (STB_WEAK
) であること。 - シンボルが未定義 (
SHN_UNDEF
) でないこと。 - シンボル名が目的のシンボル名と一致すること(
runtime·strcmp
を使用)。 - バージョン情報がある場合、シンボルバージョンが一致すること。
- シンボルタイプが関数 (
- 全ての条件を満たすシンボルが見つかった場合、そのシンボルのアドレスを計算し、対応するGoランタイムの変数ポインタ(例:
runtime·__vdso_time_sym
)に格納します。
- Goランタイムがルックアップしたい各シンボル(例:
この変更により、Linuxカーネル3.16以降でvDSOのセクションヘッダや通常のシンボルテーブルが利用できなくなった場合でも、GoランタイムがvDSO関数を正しくルックアップできるようになります。
コアとなるコードの変更箇所
src/pkg/runtime/vdso_linux_amd64.c
-
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;
-
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 }, };
-
vdso_init_from_sysinfo_ehdr
関数の変更:- セクションヘッダ関連のコード削除。
DT_HASH
の処理追加。- ハッシュテーブルの
nbucket
とnchain
の読み込みと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; }
-
vdso_parse_symbols
関数の変更:- シンボルルックアップロジックをハッシュテーブルベースに変更。
sym_keys[i].sym_hash
を使用。vdso_info->bucket
とvdso_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ハッシュテーブルの構造を直接反映するbucket
、chain
、nbucket
、nchain
が追加されました。これにより、ハッシュテーブルベースのシンボルルックアップに必要な情報が全てこの構造体内に集約されます。
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->valid
をtrue
に設定するようになりました。これは、ハッシュテーブルがシンボルルックアップに必須となったためです。
vdso_parse_symbols
関数
この関数は、初期化されたvDSO情報を使って、目的のシンボル(関数)をvDSO内で検索し、そのアドレスをGoランタイムの対応する変数に設定します。
- ハッシュテーブルベースの検索: 従来の線形探索に代わり、ELFハッシュテーブルを利用した効率的な検索ロジックが導入されました。
- ルックアップしたいシンボル(
sym_keys[i]
)のsym_hash
値を取得します。 sym_hash
をvdso_info->nbucket
で割った剰余を計算し、その結果をインデックスとしてvdso_info->bucket
配列からチェーンの先頭インデックスを取得します。- 取得したチェーンインデックスから、
vdso_info->chain
配列を辿って、同じハッシュ値を持つシンボルを順に検査します。 - 各シンボルについて、タイプ(関数であること)、結合(グローバルまたは弱いこと)、セクションインデックス(未定義でないこと)、そして名前の一致を確認します。
- バージョン情報がある場合は、
vdso_info->versym[chain]
(チェーンインデックスに対応するバージョン情報)と目的のバージョンが一致するかを確認します。
- ルックアップしたいシンボル(
- アドレスの格納: 全ての条件を満たすシンボルが見つかった場合、そのシンボルのアドレス(
vdso_info->load_offset + sym->st_value
)を計算し、*sym_keys[i].var_ptr
に格納します。これにより、GoプログラムからvDSO関数を呼び出せるようになります。
これらの変更により、GoランタイムはLinuxカーネルのvDSO構造の進化に対応し、将来にわたって安定してvDSO関数を利用できるようになりました。
関連リンク
- Go CL: https://golang.org/cl/101260044
- Linux vDSO parse_vdso.c: https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/Documentation/vDSO/parse_vdso.c
- ELF Dynamic Linking Spec: http://sco.com/developers/gabi/latest/ch5.dynamic.html
- LSB 3.2.0 Symbol Versioning: http://refspecs.linuxfoundation.org/LSB_3.2.0/LSB-Core-generic/LSB-Core-generic/symversion.html
参考にした情報源リンク
- Linux vDSO - Wikipedia
- ELF (Executable and Linkable Format) - Wikipedia
- Dynamic linker - Wikipedia
- System V Application Binary Interface - DRAFT - 24 February 1997 - Chapter 4. Object Files (ELF Header, Program Header, Section Header)
- System V Application Binary Interface - DRAFT - 24 February 1997 - Chapter 5. Program Loading and Dynamic Linking (Dynamic Section, Hash Table)
- Linux kernel 3.16 release notes or changelog (relevant sections for VDSO changes) (具体的なvDSOの変更点に関する公式ドキュメントやリリースノートは、カーネルのメーリングリストやコミットログを深く掘り下げる必要がありますが、一般的にはこのようなリリースノートで概要が提供されます。)
- Go Issue #8197: runtime: vdso lookup fails on Linux 3.16 (このコミットが修正する問題のGitHub Issue)