📜 ⬆️ ⬇️

Access to variables Thread local storage (TLS) of any thread

This article illustrates how to access variables from Thread Local Storage in Delphi. However, the principles of finding a “foreign” TLS block are the same for all Windows compilers and are applicable to any programming language that supports TLS as defined by Windows.

In Delphi, in contrast to global variables, variables declared in a threadvar block are created for each thread with the ability to store independent values. Each thread reads and writes its own copy of the values.
But sometimes it is necessary to read or even change the variables corresponding to another thread.
Of course, it is better to change the algorithm to avoid such a need, but there is a solution to this problem.
All data blocks (Thread local storage, TLS) are in memory at the same time, but at different addresses, each thread stores a pointer to its memory area, so it is possible to find a block of variables and a specific value belonging to any thread created within the current process.

The Thread local storage area in which the values ​​are stored is determined by the value from the TEB data block. The array address is located at the tlsArray offset (declared in the SysInit.pas module).
Each time a variable declared as threadvar is accessed, an implicit call to the _GetTls function occurs, which returns a pointer to the data area of ​​the current thread. By adding the offset of the variable, you can get its address.

To get the address of a variable from another thread, you need to subtract the address of the current block and add the block address of the target thread.
It is simply impossible to call the _GetTls utility function, you need to call it from the assembly code by adding the @ symbol in front of the name and the name of the System module.
')
function GetCurrentTls: Pointer; asm call System.@GetTls end; 


The same method is suitable for calling most utility functions, whose name begins with an underscore, which are not called in the usual way in Delphi code:

 call System.@ 


First, we write the following function:

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


The function takes as arguments a descriptor (not an identifier!) Of the thread we need and the address of a variable in the current block of thread variables. The function returns the address of the same variable, but related to another thread.

In the first line we got the offset of the variable relative to the beginning of the TLS block.
Now we need to add this offset to the pointer of the area of ​​local variables of the target thread, which is stored in the TEB data block.

The displacement of the TEB thread in the general virtual space of the process can be obtained by calling the NtQueryInformationThread function. This feature is among the Windows Native functions found in the ntdll.dll library.
To use it, you can connect the JwaNative.pas module from the JEDI Win32 API set or place an external function declaration with such a prototype directly into the current module (you need to connect a standard Windows.pas module):

 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'; 


Together with getting the address of the TEB, the function will now look like this:

 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; 


It now remains to find the TLS block in the PEB structure. We look at the source codes of SysInit, specifically the _GetTls function.

In a 32-bit OS, the TLS address of the array (in which the address of the thread data area is located under the TlsIndex index) is defined by the following code:

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


For 64-bit one:

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


A simple check can show that the code for the 64-bit version also works in the 32-bit version, if we take into account another tlsArray value, as well as the fact that the TEB is located at GS: [0], and not FS [0] as in 32-bit windows.

Since we already have the TEB address (the TebBaseAddress field from the basic structure), which is equal to the beginning of the GS segment for Win64 and the FS segment for Win32, we can replace the value of @GSSegBase with the received TEB pointer

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


The full function with some optimization might look like this:

 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; 


For ease of use of this function in the code, create a class with several static methods:

 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; 


Then we can declare the following two methods:

 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; 


When using parameterized types, there are difficulties in declaring a pointer to type T.
In such cases, you can use constructions of this type:

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


Delphi resolves the untyped pointer dereferencing if a type conversion occurs immediately or if such a value without a type is passed to the functions like FillChar and Move (in which the arguments are also declared without type).

Now you can use the following code to access the “foreign” thread variable:

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


And adding this declaration after the TThreadLocalStorage class:

 type TLS = TThreadLocalStorage; 


You can also shorten the code:
  X := TLS.GetThreadVar<Integer>( Thrd, TlsX ); 


As a conclusion, it should be noted that when accessing a variable from another thread, one should remember about synchronization, as when accessing global variables accessible to all threads. This is especially true for Inc, Dec operations, as well as for composite data types. The lack of need to synchronize access to threadvar data was due only to the fact that all other threads did not have access to the data of the current thread.

Source: https://habr.com/ru/post/157783/


All Articles