Access to thread local storage (TLS) variables of any thread
- Tutorial
In Delphi, unlike global variables, variables declared in the threadvar block are created for each thread with the ability to store independent values. Each stream reads and writes its 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 variable block and a specific value that belongs 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 address of the array is located at offset tlsArray (declared in the SysInit.pas module).
Each time the variable declared as threadvar is accessed, the _GetTls function is implicitly called, which returns a pointer to the data area of the current thread. By adding a variable offset, you can get its address.
To get the address of a variable from another thread, subtract the address of the current block and add the address of the block of the target thread.
It is simply impossible to call the _GetTls utility function, you need to call it from the assembler 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, the name of which begins with an underscore, which are not called in the usual way in Delphi code:
call System.@НазваниеФункцииБезПодчеркивания
To begin with, write a function like this:
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 the variable in the current block of thread variables. The function will return 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 you need to add this offset to the pointer of the local variables of the target thread, which is stored in the TEB data block.
The offset of the TEB thread in the overall virtual space of the process can be obtained by calling the NtQueryInformationThread function. This function is one of the Windows Native functions that are in the ntdll.dll library.
To use it, you can connect the JwaNative.pas module from the JEDI Win32 API set or place the external function declaration with such a prototype directly in the current module (you need to connect the 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 TEB address, 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;
Now it 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 address of the TLS array (in which under the TlsIndex index is the address of the thread data area) is determined by this code:
MOV EAX,TlsIndex
MOV EDX,FS:[tlsArray]
MOV EAX,[EDX+EAX*4]
For 64-bit like this:
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 one, if we take into account the different tlsArray value, and also that the TEB is located at GS: [0], not FS [0] as in 32-bit Windows.
Since we already have the TEB address (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 TEB pointer we received
Tls := PPPointerArray( PByte( basic.TebBaseAddress ) + tlsArray )^;
A 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 the convenience of using 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( hThread: THandle; var TlsVar: T ): T; static;
class procedure SetThreadVar( 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( hThread: THandle; var TlsVar: T ): T;
begin
Result := T( GetTlsAddress( hThread, @TlsVar )^ );
end;
class procedure TThreadLocalStorage.SetThreadVar( hThread: THandle; var TlsVar: T; const Value: T );
begin
T( GetTlsAddress( hThread, @TlsVar )^ ) := Value;
end;
When using parameterized types, it is difficult to declare a pointer to a type T.
In such cases, you can use constructions of this type:
X := T(PointerVar^);
T(PointerVar^) := X;
Delphi allows dereferencing of an untyped pointer if a type conversion occurs immediately or if such a value without a type is passed to functions like FillChar and Move (in which the arguments are also declared without a type).
Now, to access the variable of a “foreign” thread, you can use the following code:
threadvar TlsX;
...
TThreadLocalStorage.GetThreadVar( Thrd, TlsX );
And adding such an declaration after the TThreadLocalStorage class:
type
TLS = TThreadLocalStorage;
You can also shorten the code:
X := TLS.GetThreadVar( Thrd, TlsX );
In 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 absence of the 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.