鎖和同步對象

介紹

  歡迎來到這個系列的第7篇。在這部分中,我們將學習一些關於鎖和同步對象的知識。在這篇教程中,我們會稍作改變,我們將同時使用用戶態調試器和內核態調試器。這樣我們就能更好的瞭解他們。

 

什麼是死鎖

  這是多線程中最基本的一個問題。當一個線程因爲一個對象的無限期鎖住,永遠不可能得到一個資源,比如臨界區,鎖,或其他種類的同步對象,這就造成了死鎖。最簡單的例子就是有兩個線程,兩個鎖,鎖A用於從數據庫讀數據,鎖B用於從數據庫寫數據。

線程1獲得鎖A,並從數據庫中讀數據,線程2同時得到鎖B,並向數據庫寫數據。在沒有釋放各自的鎖之前,線程1想得到鎖B來寫數據,線程2想得到鎖A來讀數據。兩個線程都不可能完成,因爲它們各自擁有對方的鎖。這是一個簡單的例子。

  這並不是死鎖的唯一形式。可能一個線程在等待一個事件,另一個會將這個事件置位的線程卻已經退出了,這個線程也會形成死鎖。

  還有其他很多類型。解決這個問題的基本步驟如下。

1. 找到誰正在佔有這個資源

2. 找出是否這個佔有線程在等待其它資源

3. 找出當前線程擁有的資源

基本上就是找出哪些線程擁有哪些資源,哪些線程在等待哪些資源。一旦找到這些之後,問題很快就能解決。

 

用戶態下可用的對象

  用戶態下,我們能使用哪些對象呢?在這部分中,我們將探索一下。

 

事件

  時間就是一個有信號狀態的對象。沒人能佔有一個時間,但是它卻能用於同步。我們可以給事件命名,也就是說時間是全局的,許多進程都能打開它。也可以是沒有名字的,這樣只能在這個進程空間中看見。

  時間可以是人工重置,也可以是自動重置的。自動重置就是說一旦等待線程得到信號之後,系統自動將時間置爲無信號狀態。如果是人工重置,線程就必須自己將其置爲無信號。在內核中創建事件使用KeInitializeEvent,並且事件可以是通知類型的或同步類型的,你可以在察看句柄信息的時候看到這個類型。通知類型是人工重置的,同步類型是自動重置的。如果你想得到更多關於事件的信息,可以在MSDN中查詢關於CreateEventKeInitializeEvent的相關信息。

  和其它大部分東西一樣,在用戶態中,事件僅僅是一個句柄,因此我們可以使用!handle

0:001> !handle 7e4 ff

Handle 7e4

  Type          Event

  Attributes    0

  GrantedAccess 0x1f0003:

         Delete,ReadControl,WriteDac,WriteOwner,Synch

         QueryState,ModifyState

  HandleCount   2

  PointerCount  4

  Name          <NONE>

  Object Specific Information

    Event Type Auto Reset

    Event is Set

你可以看到,這個事件當前是置位狀態,並且是一個自動重置事件。事件作爲同步對象來使用的一個真實例子就是DBGVIEW,我們來看看OutputDebugString

Application

1. Acquire Mutex

2. Open Memory Mapped File

3. Wait for Buffer is Ready event

4. Write to Memory Mapped File

5. Signal Buffer Data Available event

6. Close Handles

 

DbgView

1. Wait for Buffer Data Available event

2. Read Buffer Data.

3. Signal Buffer is ready event

4. Goto 1

你可以看到,互斥量是用來保護內存影射文件的寫入的,而這兩個事件是用來同步程序和DBGVIEW對這個文件的讀寫的。

 

互斥量

  互斥量是能通過名字使用的全局同步對象。也就是說,多個程序可以使用同一個互斥量,它是存在於內核中的。互斥量一次只允許一個線程獲得。

0:005> !handle 2c0 ff

Handle 2c0

  Type          Mutant

  Attributes    0

  GrantedAccess 0x1f0001:

         Delete,ReadControl,WriteDac,WriteOwner,Synch

         QueryState

  HandleCount   2

  PointerCount  3

  Name          <NONE>

  Object Specific Information

    Mutex is Free

0:005> !handle 2b0 ff

Handle 2b0

  Type          Mutant

  Attributes    0

  GrantedAccess 0x120001:

         ReadControl,Synch

         QueryState

  HandleCount   17

  PointerCount  19

  Name          \BaseNamedObjects\ShimCacheMutex

  Object Specific Information

    Mutex is Free

互斥量是Mutant類型的。其中一個有名字,另外一個沒有名字。

注意到,Windows2003顯示的句柄信息比Windows2000多,並且因爲某些問題,比如死鎖,請求信息的時候,不是所有的信息都會顯示。因此,內核調試器或handle.exe這樣使用驅動的程序可以直接從對象讀取信息。你可以在part 5中得到更多關於句柄的信息。

  那麼,互斥量在使用的時候是什麼樣的呢?有兩種情形,你的線程正在使用這個互斥量,或別的進程中的線程正在使用這個互斥量。

0:005> !handle 50 ff

Handle 50

  Type          Mutant

  Attributes    0

  GrantedAccess 0x1f0001:

         Delete,ReadControl,WriteDac,WriteOwner,Synch

         QueryState

  HandleCount   2

  PointerCount  3

  Name          <NONE>

  Object Specific Information

    Mutex is Owned

可以看到”Mutex is Owned”。進程中還有其他很多互斥量,你不知道它們是被佔有還是空着的,那我們該如何知道誰在佔有這個互斥量呢?

  Handle.exe也不能告訴我們佔有者,我們需要到內核中去看。

因此,我創建了一個互斥量,並調用WaitForSingleObject,然後在內核中的NtWaitForSingleObject下斷點,然後跟進去。之後,我們可以看到這個句柄對應一個對象,然後調用KeWaitForSingleObject。這個函數檢察它是否已經被佔有。

eax=00000001 ebx=fcc724a0 ecx=00000000 edx=00000000 esi=fcd19860 edi=fcd198cc

eip=8042d697 esp=fb72bcc0 ebp=fb72bce0 iopl=0         ov up ei ng nz na pe cy

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000a83

nt!KeWaitForSingleObject+0x1d5:

8042d697 ff4b04           dec   dword ptr [ebx+0x4] ds:0023:fcc724a4=00000001

...

eax=00000000 ebx=fcc724a0 ecx=00000000 edx=00000000 esi=fcd19860 edi=fcd198cc

eip=8042d6aa esp=fb72bcc0 ebp=fb72bce0 iopl=0         nv up ei ng nz ac po cy

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000297

nt!KeWaitForSingleObject+0x1e8:

8042d6aa 897318           mov     [ebx+0x18],esi    ds:0023:fcc724b8=00000000

...

kd> !object fcc724b8

Object: fcc724b8  Type: (fcc724a8) 

    ObjectHeader: fcc724a0

    HandleCount: 0  PointerCount: 524290

    Directory Object: 00000001  Name: (*** Name not accessable ***)

kd> dd fcc724b8 l1

fcc724b8  fcd19860

kd> !thread fcd19860 1

THREAD fcd19860  Cid 214.418  Teb: 7ffdc000  Win32Thread: 00000000 RUNNING

EBX指向了對象頭。因此ObjectHeader+18就是擁有線程。

 

信號量

  和其它一樣,這個也是有內核對象的。唯一不同的是,信號量可以有多於1個的計數,而互斥量一次只能被一個線程佔有。

0:002> !handle 0 f semaphore

Handle 90

  Type          Semaphore

  Attributes    0

  GrantedAccess 0x1f0003:

         Delete,ReadControl,WriteDac,WriteOwner,Synch

         QueryState,ModifyState

  HandleCount   2

  PointerCount  3

  Name          <NONE>

  Object Specific Information

    Semaphore Count 15

    Semaphore Limit 16

1 handles of type Semaphore

從用戶態調試器中,我們可以看到有多少個信號量,還有他們可以擁有的線程數量。0就表示可用的都用完了。用戶態調試器也把信號量和互斥量看作不同的對象。下面我們來看看我們在這個對象中發現什麼。

kd> !handle 90 ff fccde020

processor number 0

PROCESS fccde020  SessionId: 0  Cid: 03cc    Peb: 7ffdf000  ParentCid: 03d8

    DirBase: 00e7b000  ObjectTable: fccbfae8  TableSize:  36.

    Image: mspaint.exe

 

Handle Table at e1e62000 with 36 Entries in use

0090: Object: fcec1680  GrantedAccess: 001f0003

Object: fcec1680  Type: (fcebd620) Semaphore

    ObjectHeader: fcec1668

        HandleCount: 1  PointerCount: 1

 

kd> dd fcec1668 

fcec1668  00000001 00000001 fcebd620 00000000

fcec1678  fcc75888 00000000 00050005 00000010

fcec1688  fcec1688 fcec1688 00000010 7ffddfff

fcec1698  00000000 00000000 03018002 644c6d4d

fcec16a8  fcec19a8 fce73b48 00650074 0052006d

fcec16b8  006f006f 005c0074 fc8f5000 fc90b87c

fcec16c8  00025000 00500050 e134a588 00160016

fcec16d8  fcec87a8 09104000 004b0002 ffffffff

上面是我們獲得對象之前,對象頭的一個Dump。下面我們來獲取這個對象,看看會發生什麼。

kd> !handle 90 ff fccde020

processor number 0

PROCESS fccde020  SessionId: 0  Cid: 03cc    Peb: 7ffdf000  ParentCid: 03d8

    DirBase: 00e7b000  ObjectTable: fccbfae8  TableSize:  36.

    Image: mspaint.exe

 

Handle Table at e1e62000 with 36 Entries in use

0090: Object: fcec1680  GrantedAccess: 001f0003

Object: fcec1680  Type: (fcebd620) Semaphore

    ObjectHeader: fcec1668

        HandleCount: 1  PointerCount: 1

 

kd> dd fcec1668

fcec1668  00000001 00000001 fcebd620 00000000

fcec1678  fcc75888 00000000 00050005 0000000f

fcec1688  fcec1688 fcec1688 00000010 7ffddfff

fcec1698  00000000 00000000 03018002 644c6d4d

fcec16a8  fcec19a8 fce73b48 00650074 0052006d

fcec16b8  006f006f 005c0074 fc8f5000 fc90b87c

fcec16c8  00025000 00500050 e134a588 00160016

fcec16d8  fcec87a8 09104000 004b0002 ffffffff

可以看到,計數減少了,因此我們可以看到信號量的數量和現在可用的數量的存儲地址。另外我們還是不知道哪個線程正在佔有這個信號量,我們該如何找到他呢?

  經過一番調試,我們發現上面的代碼並不適用於信號量,線程上下文並沒有保存下來。很不幸,信號亮並不會被一個或一堆線程佔有,它們只是基於計數工作的。

  事實上,如果你在同一個線程中調用幾次WaitForSingleObject後,計數也在減少。這就意味着,編程的時候你必須注意,因爲你可能不小心的就釋放了信號量,即使你並沒有自己去減少這個計數。

 

臨界區

  另外還有用戶態的數據結構來表示同步對象。它們用來在同一個進程中對一個資源進行保護。由於它們是用戶態的,所以調試起來很簡單。

  我們可以找到誰在佔有它,你還可以使用!locks!locks –v來得到和臨界區有關的信息。

0:000> !critsec ntdll!LdrpLoaderLock

 

CritSec ntdll!LdrpLoaderLock+0 at 77FC1774

LockCount          0

RecursionCount     1

OwningThread       9ac

EntryCount         0

ContentionCount    0

*** Locked

最有名的就是Windowsloader鎖。當你加載庫,創建或銷燬線程的時候它會鎖住。我們可以看到,這個鎖現在正處於鎖住狀態。我們可以使用!critsec來得到這個鎖的信息。來看看誰在擁有這個線程。

0:001> ~*

   0  Id: e28.9ac Suspend: 1 Teb: 7ffde000 Unfrozen

      Start: notepad!WinMainCRTStartup (01006ae0)

      Priority: 0  Priority class: 32

.  1  Id: e28.aa0 Suspend: 1 Teb: 7ffdd000 Unfrozen

      Start: ntdll!DbgUiRemoteBreakin (77f5f2f2)

      Priority: 0  Priority class: 32

這個進程中只有兩個線程,找到這個線程很簡單。

0:001> !critsec ntdll!LdrpLoaderLock

 

CritSec ntdll!LdrpLoaderLock+0 at 77FC1774

LockCount          NOT LOCKED

RecursionCount     0

OwningThread       0

EntryCount         0

ContentionCount    0

這是當臨界區沒有鎖住時的輸出。臨界區相對於互斥量還有什麼優勢呢?因爲它們是用戶態的,因此它們不用進入內核,也就不用和其它進程去爭搶。這讓它實現起來更快,調試的時候更簡單。對這個地址使用dd命令,你也可以看到這個數據結構很簡單。

  如果這個線程不存在,那麼這個線程就是在釋放這個鎖之前就已經退出了。

 

內核態可用的對象

  這裏我們講述一些內核態下的對象。事件,信號量,互斥量都能在內核態下使用。事實上,用戶態的程序也都是進入內核態,使用相同的函數然後創建這些對象的。另外還有其他一些內核態的對象。它們是自旋鎖和ERESOURCE

 

自旋鎖

  自旋鎖是用來在多處理器系統中保護資源的一種同步對象。自旋鎖和臨界區的不同之處在於,第二個處理器會在這個鎖上一直自旋知道獲取它,而不是讓其他線程去運行。

  在單處理器系統上,自旋鎖只是將IRQL增高,以使線程不能切換,讓代碼運行。但是你也不能使用分頁內存,並且你只能進行一小部分操作。

hal!KfAcquireSpinLock:

80069850 33c0             xor     eax,eax

80069852 a024f0dfff       mov     al,[ffdff024]

80069857 c60524f0dfff02   mov     byte ptr [ffdff024],0x2

8006985e c3               ret

這是指向全局IRQL的一個地址。調用KeAcquireSpinLock會將IRQL置爲2。然後它保存以前的IRQL,用來在KeReleaseSpinLock中恢復。

  IRQL表示操作系統當前處於的一箇中斷級。下面是NTDDK中的定義。

#define PASSIVE_LEVEL 0             // Passive release level

#define LOW_LEVEL 0                 // Lowest interrupt level

#define APC_LEVEL 1                 // APC interrupt level

#define DISPATCH_LEVEL 2            // Dispatcher level

因此,自旋鎖會把操作系統升到DISPATCH_LEVEL。你可以在MSDN中找到更多關於IRQL的信息。

自旋鎖函數在多處理器系統中會有一些不同。它們會一直自旋直到獲得它。

“LOCK”這個彙編指令會鎖住總線,防止其他處理器讀或寫同一塊內存區域。以0爲參數使用BTS指令,就是把0放入一個carry flag,然後把這個0位置爲1

JB指令會在carry flag1(也就是說它以前是1)跳轉。如果0位是1,它會做一個test,如果0位不是1,它就會跳回去然後再繼續。如果0位是1,就意味着它正在被佔有,因此它會做一個”pause”。

hal!KfAcquireSpinLock:

80065420 8b158000feff     mov     edx,[fffe0080]

80065426 c7058000feff41000000 mov dword ptr [fffe0080],0x41

80065430 c1ea04           shr     edx,0x4

80065433 0fb68280a30680   movzx   eax,byte ptr [edx+0x8006a380]

8006543a f00fba2900       lock    bts dword ptr [ecx],0x0

8006543f 7203             jb      hal!KfAcquireSpinLock+0x24 (80065444)

80065441 c3               ret

80065442 8bff             mov     edi,edi

0: kd> u

hal!KfAcquireSpinLock+0x24:

80065444 f70101000000     test    dword ptr [ecx],0x1

8006544a 74ee             jz      hal!KfAcquireSpinLock+0x1a (8006543a)

8006544c f390             pause

8006544e ebf4             jmp     hal!KfAcquireSpinLock+0x24 (80065444)

這是自旋鎖工作的本質,事實上並沒有很多工作,並且大部分程序都不會用到自旋鎖。一般情況下,信號量或互斥量已經足夠了。如果你想使用自旋鎖,可以去閱讀MSDN。另外還有”queued”自旋鎖,可以提供更好的表現。

 

The ERESOURCE

  下面,我來解釋ERESOURCE

A. 你可以讀MSDN,我不想重複已有的信息

B. 如果你不瞭解他們,你可能不會使用它們,也不需要調試它們。這是調試文章,而不是編程文章。

但是,我會大概的介紹以下。ERESOURCE是允許你在內核中共享去獨佔訪問的一個數據結構。共享的意思是多個線程可以獲得它,獨佔的意思是隻有一個線程可以獲得它。

  需要注意的是,ERESOURCE存在於非分頁內存的一個全局鏈表中。這就是說,如果你釋放這塊內存或覆蓋掉這個數據結構,可能會造成崩潰。

  在內核調試器中,你可以使用!locksDump出所有的系統中所有的鎖。

kd> !locks

**** DUMP OF ALL RESOURCE OBJECTS ****

KD: Scanning for held locks.....................................

 

Resource @ 0xfceba0c0    Shared 1 owning threads

     Threads: fcebeda3-01<*> *** Actual Thread FCEBEDA0

KD: Scanning for held locks.

1814 total locks, 1 locks currently held

  這裏會顯示哪個線程正在佔有這個鎖,還會顯示正在這個鎖上等待的所有線程。你可以使用!locks <address>得到信息,還可以設置一些標誌,比如!locks –v

  我們得到的最重要的信息就是!locks可以列出佔有線程和等待線程,因此調試這些會很簡單。上面看到的那個鎖只有一個佔有者,並且沒有等待者。在Windows XP/2003,你還可以使用dt _ERESOURCE顯示內部信息。

 

總結

  這裏我們已經概括了用戶態和內核態常用的同步對象。另外還有其他很多方法,比如InterlockedDecrementLockFile和其他一些程序員自己實現的同步方法。這裏我們不會講述這些,那需要你自己去解決和調試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章