任意のスレッドのスレッドローカルストレージ(TLS)変数へのアクセス

この記事では、DelphiのThread Local Storageブロックから変数にアクセスする方法を説明します。 ただし、「外部」TLSブロックを見つける原則はすべてのWindowsコンパイラで同じであり、Windowsが定義する形式でTLSをサポートするすべてのプログラミング言語に適用されます。



Delphiでは、グローバル変数とは異なり、threadvarブロックで宣言された変数は、独立した値を保存する機能を備えたスレッドごとに作成されます。 各ストリームは、値のコピーを読み書きします。

ただし、別のスレッドに対応する変数を読み取ったり、変更する必要がある場合もあります。

もちろん、そのような必要性を避けるためにアルゴリズムを変更する方が良いですが、この問題の解決策があります。

すべてのデータブロック(スレッドローカルストレージ、TLS)は同時にメモリ内にありますが、異なるアドレスでは、各スレッドはそのメモリ領域へのポインタを格納するため、現在のプロセス内で作成されたスレッドに属する変数ブロックと特定の値を見つけることができます。



値が格納されるスレッドローカルストレージ領域は、TEBデータブロックの値によって決まります。 配列のアドレスは、オフセットtlsArrayにあります(SysInit.pasモジュールで宣言されています)。

threadvarとして宣言された変数にアクセスするたびに、_GetTls関数が暗黙的に呼び出され、現在のスレッドのデータ領域へのポインターが返されます。 可変オフセットを追加することにより、そのアドレスを取得できます。



別のスレッドから変数のアドレスを取得するには、現在のブロックのアドレスを減算し、ターゲットスレッドのブロックのアドレスを追加します。

単に_GetTlsユーティリティ関数を呼び出すことは不可能です。名前とシステムモジュールの名前の前に@記号を追加して、アセンブラコードから呼び出す必要があります。



function GetCurrentTls: Pointer; asm call System.@GetTls end;
      
      







同じメソッドは、アンダースコアで始まるほとんどのユーティリティ関数の呼び出しに適しています。これらの関数は、Delphiコードでは通常の方法では呼び出されません。



 call System.@
      
      







まず、次のような関数を作成します。



 function GetTlsAddress( hThread: THandle; Addr: Pointer ): Pointer; var Offset: NativeInt; begin Offset := ( PByte( Addr ) - PByte( GetCurrentTls ) ); Result := (?) + Offset; end;
      
      







この関数は引数として、必要なスレッドの記述子(識別子ではありません!)とスレッド変数の現在のブロック内の変数のアドレスを受け取ります。 この関数は、同じ変数のアドレスを返しますが、別のスレッドに関連しています。



最初の行では、TLSブロックの先頭を基準とした変数のオフセットを取得しました。

次に、このオフセットを、TEBデータブロックに格納されているターゲットスレッドのローカル変数のポインターに追加する必要があります。



プロセスの共通仮想空間でのTEBスレッドのオフセットは、NtQueryInformationThread関数を呼び出すことで取得できます。 この関数は、ntdll.dllライブラリにあるWindowsネイティブ関数の1つです。

これを使用するには、JEDI Win32 APIセットからJwaNative.pasモジュールを接続するか、現在のモジュールにそのようなプロトタイプを含む外部関数宣言を直接配置します(標準のWindows.pasモジュールを接続する必要があります)。



 type THREAD_BASIC_INFORMATION = record ExitStatus: ULONG{NTSTATUS}; TebBaseAddress: Pointer{PNT_TIB}; {ClientId: ; //  ,      AffinityMask: ; //   ,      Priority: ; //  .        TebBaseAddress BasePriority: ;} end; function NtQueryInformationThread( ThreadHandle : THandle; ThreadInformationClass : ULONG {THREADINFOCLASS}; ThreadInformation : PVOID; ThreadInformationLength : ULONG; ReturnLength : PULONG ): ULONG; stdcall; external 'ntdll.dll';
      
      







TEBアドレスを取得すると、関数は次のようになります。



 function GetTlsAddress( hThread: THandle; Addr: Pointer ): Pointer; var basic: THREAD_BASIC_INFORMATION; Len: ULONG; Offset: NativeInt; begin NtQueryInformationThread( hThread, 0{ThreadBasicInformation}, @basic, SizeOf( basic ), @Len ); Offset := ( PByte( Addr ) - PByte( GetCurrentTls ) ); Result := (?) + Offset; end;
      
      







これで、PEB構造内でTLSブロックを見つけることができます。 SysInitのソースコード、特に_GetTls関数を調べます。



32ビットOSでは、TLS配列のアドレス(TlsIndexインデックスの下でスレッドデータ領域のアドレス)は、次のコードによって決定されます。



  MOV EAX,TlsIndex MOV EDX,FS:[tlsArray] MOV EAX,[EDX+EAX*4]
      
      







このような64ビットの場合:



  P := PPPointerArray(PByte(@GSSegBase) + tlsArray)^; Result := P^[TlsIndex];
      
      







単純なチェックでは、tlsArrayの異なる値と、TEBがFS [0]ではなくGSにあるという事実を考慮すると、64ビットバージョンのコードが32ビットバージョンでも機能することがわかります。 32ビットWindowsと同様。



既にTEBアドレス(基本構造のTebBaseAddressフィールド)があり、これはWin64のGSセグメントとWin32のFSセグメントの先頭に等しいので、@ GSSegBase値を受け取ったTEBポインターに置き換えることができます。



  Tls := PPPointerArray( PByte( basic.TebBaseAddress ) + tlsArray )^;
      
      







最適化された完全な関数は次のようになります。



 function GetTlsAddress( hThread: THandle; Addr: Pointer ): Pointer; var basic: THREAD_BASIC_INFORMATION; Len: ULONG; Tls: PPointerArray; begin if hThread = GetCurrentThread then Exit( Addr ); NtQueryInformationThread( hThread, 0{ThreadBasicInformation}, @basic, SizeOf( basic ), @Len ); Tls := PPPointerArray( PByte( basic.TebBaseAddress ) + tlsArray )^; Result := PByte( Tls^[TlsIndex] ) + ( PByte( Addr ) - PByte( GetCurrentTls ) ); end;
      
      







コードでこの関数を使用するために、いくつかの静的メソッドを持つクラスを作成します。



 type TThreadLocalStorage = class private class function GetTlsAddress( hThread: THandle; Addr: Pointer ): Pointer; static; public class function GetThreadVar<T>( hThread: THandle; var TlsVar: T ): T; static; class procedure SetThreadVar<T>( hThread: THandle; var TlsVar: T; const Value: T ); static; class property Tls[hThread: THandle; Addr: Pointer]: Pointer read GetTlsAddress; end;
      
      







次に、次の2つのメソッドを宣言できます。



 class function TThreadLocalStorage.GetThreadVar<T>( hThread: THandle; var TlsVar: T ): T; begin Result := T( GetTlsAddress( hThread, @TlsVar )^ ); end; class procedure TThreadLocalStorage.SetThreadVar<T>( hThread: THandle; var TlsVar: T; const Value: T ); begin T( GetTlsAddress( hThread, @TlsVar )^ ) := Value; end;
      
      







パラメーター化された型を使用する場合、型Tへのポインターを宣言することは困難です。

そのような場合、このタイプの構造を使用できます。



 X := T(PointerVar^); T(PointerVar^) := X;
      
      







Delphiでは、型変換がすぐに発生する場合、または型のない値がFillCharやMoveなどの関数に渡される場合(型も引数も宣言されていない場合)、型なしポインタの逆参照を許可します。



ここで、「外部」スレッドの変数にアクセスするには、次のコードを使用できます。



 threadvar TlsX; ... TThreadLocalStorage.GetThreadVar<Integer>( Thrd, TlsX );
      
      







そして、TThreadLocalStorageクラスの後にそのような宣言を追加します。



 type TLS = TThreadLocalStorage;
      
      







コードを短くすることもできます:

  X := TLS.GetThreadVar<Integer>( Thrd, TlsX );
      
      







結論として、別のスレッドから変数にアクセスするときは、すべてのスレッドからアクセス可能なグローバル変数にアクセスするときのように、同期について覚えておく必要があることに注意してください。 これは、Inc、Dec操作、および複合データ型の場合に特に当てはまります。 threadvarデータへのアクセスを同期する必要がないのは、他のすべてのスレッドが現在のスレッドのデータにアクセスできなかったという事実のみによるものです。



All Articles