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

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

このコミットは、Go言語のリンカ (cmd/ld) において、生成されるELF (Executable and Linkable Format) およびMach-O (Mach Object) 形式のシンボルテーブル内で使用される特殊文字「·」(ミドルドット)を「.」(ピリオド)に置換する変更を導入します。この変更の主な目的は、macOSやFreeBSDに搭載されている古いバージョンのDTraceが、シンボル名に含まれるUnicode文字を正しく処理できない問題に対処し、GoプログラムのDTrace互換性を向上させることです。

コミット

Go言語のリンカが生成するELFおよびMach-O形式のシンボルテーブルにおいて、Goの内部シンボル名で使用される「·」(ミドルドット)文字を「.」(ピリオド)に置換する。これにより、古いDTraceバージョンでのUnicodeシンボル名に関する互換性問題を解決する。

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

https://github.com/golang/go/commit/cbe777b2c70320f52f85c6c8f1242b35dd45b341

元コミット内容

commit cbe777b2c70320f52f85c6c8f1242b35dd45b341
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri Mar 14 10:07:51 2014 -0400

    cmd/gc: replace '·' as '.' in ELF/Mach-O symbol tables
    
    Old versions of DTrace (as those shipped in OS X and FreeBSD)
    don't support unicode characters in symbol names.  Replace '·'
    to '.' to make DTrace happy.
    
    Fixes #7493
    
    LGTM=aram, rsc
    R=aram, rsc, gobot, iant
    CC=golang-codereviews
    https://golang.org/cl/72280043

変更の背景

Go言語では、メソッド名や内部的なシンボル名において、型名とメソッド名を区切るために「·」(ミドルドット、Unicode U+00B7)という特殊な文字を使用する慣習があります。例えば、MyType.MyMethod のようなメソッドは、コンパイル後のシンボルテーブルでは MyType·MyMethod のように表現されることがあります。

しかし、macOSやFreeBSDなどの一部のオペレーティングシステムに搭載されている古いバージョンのDTrace(動的トレーシングフレームワーク)は、シンボル名にUnicode文字が含まれていると、それを正しく解析できないという既知の問題を抱えていました。この問題は、Goプログラムの実行時にDTraceを使用してパフォーマンス分析やデバッグを行おうとする際に、シンボルが正しく認識されない、あるいはDTraceがクラッシュするといった形で現れていました。

この互換性の問題を解決するため、GoのリンカがELFおよびMach-O形式の実行ファイルを生成する際に、シンボルテーブル内の「·」文字を、DTraceが問題なく処理できる「.」(ピリオド)に置換する必要が生じました。これにより、Go言語の内部的な命名規則を変更することなく、外部ツールとの互換性を確保することが目的です。

前提知識の解説

DTrace (Dynamic Tracing)

DTraceは、Solarisオペレーティングシステムで開発された強力な動的トレーシングフレームワークです。macOS、FreeBSD、NetBSDなどの他のUnix系システムにも移植されています。DTraceを使用すると、実行中のシステムやアプリケーションの動作を、コードを変更したり再コンパイルしたりすることなく、リアルタイムで詳細に監視・分析できます。

DTraceは、カーネルやユーザー空間の様々なプローブポイント(特定のイベントが発生したときにトリガーされる場所)にスクリプトをアタッチすることで機能します。これらのスクリプトはD言語(DTraceの専用言語)で記述され、イベント発生時のシステム状態やアプリケーションデータを収集し、分析することができます。

このコミットの文脈では、DTraceがGoプログラムのシンボル(関数名など)を正しく認識できないことが問題でした。DTraceは実行ファイルのシンボルテーブルを読み取り、関数呼び出しなどのイベントを特定のシンボルに関連付けます。シンボル名にDTraceが解釈できない文字が含まれていると、この関連付けが失敗し、トレーシングが機能しなくなります。

ELF (Executable and Linkable Format)

ELFは、Unix系オペレーティングシステム(Linux、Solarisなど)で広く使用されている実行可能ファイル、オブジェクトコード、共有ライブラリ、およびコアダンプファイルの標準ファイル形式です。ELFファイルは、プログラムのコード、データ、および実行に必要なメタデータ(シンボルテーブル、セクションヘッダなど)を構造化して格納します。

Mach-O (Mach Object)

Mach-Oは、AppleのmacOSおよびiOSオペレーティングシステムで使用されている実行可能ファイル、オブジェクトコード、共有ライブラリ、およびコアダンプファイルのファイル形式です。Mach-Oは、Machカーネルの設計に由来しており、ELFと同様にプログラムのコードとデータを構造化して格納しますが、その内部構造は異なります。

シンボルテーブル

シンボルテーブルは、実行可能ファイルやオブジェクトファイル内に含まれる重要なデータ構造の一つです。これには、プログラム内の関数、グローバル変数、静的変数などの「シンボル」に関する情報が格納されています。各シンボルには、その名前、型(関数、変数など)、アドレス、サイズなどの属性が関連付けられています。

デバッガやプロファイラ、トレーシングツール(DTraceなど)は、シンボルテーブルを利用して、実行中のプログラムの特定のメモリ位置や命令を、人間が理解しやすいシンボル名(例: main 関数、myGlobalVar 変数)に関連付けます。これにより、開発者はソースコードレベルでプログラムの動作を分析できるようになります。

Go言語における「·」(ミドルドット)の使用

Go言語のコンパイラとリンカは、内部的に特定のシンボル名にUnicode文字の「·」(ミドルドット、U+00B7)を使用します。最も一般的な例は、構造体やインターフェースのメソッド名です。例えば、type MyStruct struct {}func (m MyStruct) MyMethod() {} というメソッドがある場合、そのシンボル名は MyStruct·MyMethod のように表現されます。これは、Goのツールチェーンが内部的に型とメソッドを区別するための慣習であり、Goのソースコード上では通常のピリオド(.)として記述されます。

この「·」は、UTF-8エンコーディングでは2バイト(\xc2\xb7)で表現されます。古いDTraceバージョンがこのUnicodeシーケンスを正しく解釈できないことが、今回の問題の根源でした。

技術的詳細

このコミットは、Goリンカの2つの主要なファイル、src/cmd/ld/macho.csrc/cmd/ld/symtab.c に変更を加えることで、シンボルテーブル内の「·」文字を「.」に置換します。

src/cmd/ld/macho.c の変更

macho.c はMach-O形式の実行ファイルを生成する際のシンボルテーブル関連の処理を担当します。具体的には、machosymtab 関数が変更されています。

変更前は、シンボルの外部名 (s->extname) をそのまま symstr (シンボル文字列テーブル) に追加していました。変更後は、s->extname に「·」が含まれているかどうかをチェックします。

  • strstr(s->extname, "·") == nil: シンボル名に「·」が含まれていない場合、従来通り addstring(symstr, s->extname) で文字列を追加します。
  • strstr(s->extname, "·") != nil: シンボル名に「·」が含まれている場合、文字列を1バイトずつ走査し、UTF-8エンコーディングの「·」(\xc2\xb7)を見つけると、それを「.」(ピリオド)に置換して symstr に追加します。この際、\xc2\xb7 は2バイトですが、「.」は1バイトであるため、文字列の長さが1バイト短縮されることに注意が必要です。

src/cmd/ld/symtab.c の変更

symtab.c はELF形式の実行ファイルを生成する際のシンボルテーブル関連の処理、特に文字列テーブル (.strtab.dynstr) への文字列追加を担当します。具体的には、putelfstr 関数が変更されています。

putelfstr 関数は、ELF文字列テーブルに文字列を追加し、そのオフセットを返します。変更前は、入力された文字列 s をそのまま elfstrdat (ELF文字列データ) にコピーしていました。変更後は、コピー後に文字列内に「·」が含まれているかをチェックします。

  • p = strstr(s, "·"): コピーされた文字列 s 内に「·」が含まれているかを検索します。
  • 置換ロジック: 「·」が見つかった場合、文字列を走査し、UTF-8エンコーディングの「·」(\xc2\xb7)を「.」(ピリオド)に置換します。この際、\xc2\xb7 の2バイトを「.」の1バイトに置き換えるため、後続の文字を1バイト前方にシフトし、文字列全体のサイズを調整 (elfstrsize--) します。これにより、文字列テーブル内のデータが正しく配置されます。

これらの変更により、Goリンカは、DTraceが期待する形式でシンボル名をエクスポートするようになり、古いDTraceバージョンとの互換性が確保されます。

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

src/cmd/ld/macho.c

--- a/src/cmd/ld/macho.c
+++ b/src/cmd/ld/macho.c
@@ -574,6 +574,7 @@ machosymtab(void)
 {
  int i;
  LSym *symtab, *symstr, *s, *o;
+ char *p;
  
  symtab = linklookup(ctxt, ".machosymtab", 0);
  symstr = linklookup(ctxt, ".machosymstr", 0);
@@ -585,7 +586,21 @@ machosymtab(void)
  // Only add _ to C symbols. Go symbols have dot in the name.
  if(strstr(s->extname, ".") == nil)
  adduint8(ctxt, symstr, '_');
- addstring(symstr, s->extname);
+ // replace "·" as ".", because DTrace cannot handle it.
+ if(strstr(s->extname, "·") == nil) {
+ addstring(symstr, s->extname);
+ } else {
+ p = s->extname;
+ while (*p++ != '\0') {
+ if(*p == '\xc2' && *(p+1) == '\xb7') {
+ adduint8(ctxt, symstr, '.');
+ p++;
+ } else {
+ adduint8(ctxt, symstr, *p);
+ }
+ }
+ adduint8(ctxt, symstr, '\0');
+ }
  if(s->type == SDYNIMPORT || s->type == SHOSTOBJ) {
  adduint8(ctxt, symtab, 0x01); // type N_EXT, external symbol
  adduint8(ctxt, symtab, 0); // no section

src/cmd/ld/symtab.c

--- a/src/cmd/ld/symtab.c
+++ b/src/cmd/ld/symtab.c
@@ -40,6 +40,7 @@ static int
 putelfstr(char *s)
 {
  int off, n;
+ char *p, *q;
  
  if(elfstrsize == 0 && s[0] != 0) {
  // first entry must be empty string
@@ -54,6 +55,21 @@ putelfstr(char *s)
  off = elfstrsize;
  elfstrsize += n;
  memmove(elfstrdat+off, s, n);
+ // replace "·" as ".", because DTrace cannot handle it.
+ p = strstr(s, "·");
+ if(p != nil) {
+ p = q = elfstrdat+off;
+ while (*q != '\0') {
+ if(*q == '\xc2' && *(q+1) == '\xb7') {
+ q += 2;
+ *p++ = '.';
+ elfstrsize--;
+ } else {
+ *p++ = *q++;
+ }
+ }
+ *p = '\0';
+ }
  return off;
 }
 

コアとなるコードの解説

src/cmd/ld/macho.cmachosymtab 関数

この関数はMach-O形式のシンボルテーブルを構築する際に呼び出されます。 変更の核心は、シンボルの外部名 s->extname をシンボル文字列テーブル symstr に追加するロジックにあります。

  1. if(strstr(s->extname, "·") == nil): まず、シンボル名 s->extname にUnicodeのミドルドット「·」が含まれているかを確認します。strstr は部分文字列を検索するC標準ライブラリ関数です。
  2. 「·」が含まれていない場合: addstring(symstr, s->extname); が実行され、元のシンボル名がそのまま文字列テーブルに追加されます。
  3. 「·」が含まれている場合: else ブロックに入り、手動で文字列を走査して置換処理を行います。
    • p = s->extname;: シンボル名の先頭ポインタを p に設定します。
    • while (*p++ != '\0'): 文字列の終端まで1バイトずつ走査します。*p++ は現在の文字を評価し、その後ポインタをインクリメントします。
    • if(*p == '\xc2' && *(p+1) == '\xb7'): 現在のポインタ p が指すバイトが \xc2 で、次のバイトが \xb7 であるかをチェックします。これはUnicodeのミドルドット「·」のUTF-8エンコーディングです。
    • ミドルドットが見つかった場合:
      • adduint8(ctxt, symstr, '.');: シンボル文字列テーブルにピリオド「.」を追加します。
      • p++;: ミドルドットは2バイト文字なので、次のバイトもスキップするためにポインタをさらに1つ進めます。
    • ミドルドットではない場合:
      • adduint8(ctxt, symstr, *p);: 現在のバイトをそのままシンボル文字列テーブルに追加します。
    • ループ終了後、adduint8(ctxt, symstr, '\0'); で文字列の終端を示すヌル文字を追加します。

この手動でのバイト走査と置換は、addstring 関数が内部的にUnicode文字のバイトシーケンスをそのまま扱うため、DTraceが期待するASCII互換のシンボル名にするために必要です。

src/cmd/ld/symtab.cputelfstr 関数

この関数はELF形式の文字列テーブルに文字列を追加する際に呼び出されます。 変更の核心は、文字列が elfstrdat にコピーされた後に、その内容を走査して置換を行う点です。

  1. memmove(elfstrdat+off, s, n);: まず、入力された文字列 selfstrdat の適切なオフセットにコピーします。
  2. p = strstr(s, "·");: コピー元の文字列 s にミドルドット「·」が含まれているかを確認します。これは、置換が必要かどうかを判断するための初期チェックです。
  3. 「·」が含まれている場合: if(p != nil) ブロックに入り、置換処理を行います。
    • p = q = elfstrdat+off;: pq の両方のポインタを、文字列がコピーされた elfstrdat 内の開始位置に設定します。q は読み取りポインタ、p は書き込みポインタとして機能します。
    • while (*q != '\0'): q ポインタが文字列の終端に達するまでループします。
    • if(*q == '\xc2' && *(q+1) == '\xb7'): q が指すバイトがミドルドットのUTF-8エンコーディングであるかをチェックします。
    • ミドルドットが見つかった場合:
      • q += 2;: 読み取りポインタ q を2バイト進めて、ミドルドットの次の文字に移動します。
      • *p++ = '.';: 書き込みポインタ p が指す位置にピリオド「.」を書き込み、p を1バイト進めます。
      • elfstrsize--;: ミドルドット(2バイト)がピリオド(1バイト)に置換されたため、文字列テーブル全体のサイズを1バイト減らします。
    • ミドルドットではない場合:
      • *p++ = *q++;: q が指すバイトを p が指す位置にコピーし、両方のポインタを1バイト進めます。
    • ループ終了後、*p = '\0'; で新しい文字列の終端にヌル文字を書き込みます。

この処理は、文字列をインプレースで変更し、ミドルドットをピリオドに置換すると同時に、文字列の長さを適切に調整します。これにより、ELFシンボルテーブル内の文字列がDTrace互換になります。

関連リンク

参考にした情報源リンク