具名管道實務 ── 從設計到安全性,整理 Windows 的標準 IPC

· · Windows, 行程間通訊, Windows 開發, C#, C++, 安全性, Win32 API

「想讓常駐服務和設定畫面交換指令。」「只想把需要系統管理員權限的工作隔離到另一個處理程序。」「想讓同一台 PC 上的工具彼此傳遞資料。」── Windows 上需要這種行程間通訊(IPC)時,應優先考慮的標準就是具名管道

Windows 行程間通訊選擇的文章把具名管道定位為「同一機器 IPC 的第一候補」。本文是各論。為什麼是第一候補、模式與伺服器形態怎麼選、特權服務使用時必須守住什麼──以在 Windows 上撰寫業務應用程式與服務的開發者為對象,依一次資訊整理這些設計判斷的材料。

1. 先講結論

  • 具名管道是名稱空間為 \\.\pipe\name 的雙向行程間通道。同一個名稱下可建立多個執行個體,同時接受多個用戶端。1
  • 成為同一機器 IPC 第一候補的理由是安全性模型。可用 ACL 控制誰能連線,伺服器也能檢查並借用(偽裝)用戶端的 Windows 帳號。localhost TCP 兩者都沒有。2
  • 若想把「一次寫入=一則訊息」來處理,用訊息模式;若已有自備框架,用位元組模式。即使是訊息模式,緩衝區過短時仍要處理分割讀取(ERROR_MORE_DATA)。3
  • 多用戶端用「多個執行個體+overlapped I/O」或「.NET 的 async/await」處理。官方範例示範了以單執行緒處理多個執行個體的形態。4
  • 安全性最低限度有四點:拒絕遠端(PIPE_REJECT_REMOTE_CLIENTS)、明確指定 ACL、用 FILE_FLAG_FIRST_PIPE_INSTANCE 偵測劫持,以及在用戶端把偽裝層級降到最小。56
  • ImpersonateNamedPipeClient 的生命線是檢查傳回值。忽略失敗,處理就會以伺服器權限繼續跑。6

2. 具名管道是什麼 ── 名稱空間、執行個體、連線方式

具名管道是以 \\.\pipe\MyCompany.MyApp.Control 這類名稱識別的通道。伺服器用 CreateNamedPipe 建立,用戶端用 CreateFile 開啟同一個名稱。開啟之後雙方都以 ReadFile / WriteFile 讀寫──特色是能用和檔案 I/O 相同的形態來使用。1

重要概念是執行個體。同一個名稱的管道可建立多個執行個體,一個執行個體就是與一個用戶端的一條通道。第一次呼叫 CreateNamedPipe 時決定最大執行個體數(或無上限)。3

用戶端連線有一套標準做法。當所有執行個體都在使用中,CreateFile 會以 ERROR_PIPE_BUSY 失敗,因此用 WaitNamedPipe 等待空出來的執行個體再重試。此外,開啟時指定的存取必須符合伺服器建立時的方向──雙向管道用讀或寫都可以開啟,但伺服器只寫的輸出管道必須以唯讀開啟,伺服器只讀的輸入管道必須以唯寫開啟,否則 CreateFile 會失敗。7

管道方向與用戶端的存取指定用戶端可用讀或寫開啟雙向管道,但伺服器只寫的輸出管道必須唯讀開啟,伺服器只讀的輸入管道必須唯寫開啟雙向輸出輸入伺服器建立的方向?讀或寫都可以以唯讀開啟以唯寫開啟

圖 1: 方向與存取指定不一致會變成 CreateFile 失敗。調查連線錯誤時先看這裡。

具名管道的基本結構伺服器建立同名的多個管道執行個體,以 ConnectNamedPipe 等待連線;各用戶端用 CreateFile 開啟該名稱,與一個執行個體形成一對一雙向通道伺服器執行個體 1執行個體 2執行個體 3用戶端 A用戶端 B用戶端 C

圖 2: 持有同名的多個執行個體,一台伺服器就能同時與多個用戶端一對一交談。

具名管道也能經由 SMB 遠端開啟(\\server\pipe\name),但在現代設計裡幾乎沒有積極使用的理由;問題反而在於沒用到時不要把它留著開著(第 5 章)。

3. 位元組模式與訊息模式

管道有兩種傳輸模式。3

  • 位元組模式(PIPE_TYPE_BYTE):像 TCP 一樣的「沒有斷點的位元組串流」。一則訊息在哪裡結束由你自己決定(自行設計長度前置碼等框架)。
  • 訊息模式(PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE):一次寫入被當成一則訊息,讀取端依該單位接收。這對要求/回應的交換較輕鬆。

訊息模式還有方便的搭配函式 TransactNamedPipe,一次呼叫就能送出要求並收到回應。8 不過有陷阱。若接收緩衝區小於整則訊息,讀取會傳回 ERROR_MORE_DATA,變成分割讀取。不要假設訊息模式就等於「一次 Read 一定拿完整則」;仍要寫一個讀剩餘部分的迴圈。注意讀取模式是每個控制代碼的設定,CreateNamedPipe 只決定伺服器端。用戶端在 CreateFile 之後用 SetNamedPipeHandleState 指定(.NET 則在連線後設定 ReadMode)。7

訊息模式的分割讀取迴圈ReadFile 成功則訊息完整;若傳回 ERROR_MORE_DATA 就讀取緩衝區裝不下的剩餘部分並串接;其他錯誤視為斷線成功ERROR_MORE_DATA其他錯誤以 ReadFile 讀取結果?訊息完整讀剩餘部分並串接視為斷線

圖 3: 即使是訊息模式也需要「讀剩餘部分的迴圈」;沒有它,只有大訊息會壞掉。

位元組模式與訊息模式的差異位元組模式中三次寫入會變成沒有斷點的位元組串流,接收端必須自己切分;訊息模式保留每次寫入的單位,原樣到達接收端位元組模式:寫入 AAA、BB、CCCC收到位元組串流 AAABBCCCC框架由自己設計訊息模式:同樣三次寫入收到三則訊息:AAA、BB、CCCC寫入單位被保留

圖 4: 訊息模式保留「一次寫入的單位」並送達。框架設計變得不必要;只要別忘了處理分割讀取。

實務上怎麼選,規則很單純。交換形態是「要求與回應」就用訊息模式若承載的形式本身已內建框架(帶長度的序列化資料或串流傳輸),用位元組模式。在 .NET 中,指定 PipeTransmissionMode.Message 對應前者。9

4. 伺服器設計 ── 每個用戶端一條執行緒,還是 Overlapped?

伺服器的基本操作是「建立執行個體 → 用 ConnectNamedPipe 等用戶端 → 讀寫 → 斷線再接下一個用戶端」這個迴圈。同時和多個用戶端交談有兩種形態。

同步、每個執行個體一條執行緒。每個執行個體分配一條執行緒,各自用同步 I/O 與自己的用戶端交談。程式碼直觀,但每個用戶端消耗一條執行緒,整套關閉時也需要能從阻塞 I/O 脫身的方法。

Overlapped(非同步)。FILE_FLAG_OVERLAPPED 建立執行個體,非同步發出 ConnectNamedPipe / ReadFile / WriteFile,由少數執行緒處理所有執行個體的完成。Microsoft 官方範例示範了一個用 WaitForMultipleObjects 等待事件陣列、以單執行緒處理多個執行個體的伺服器。4 非同步 I/O 的一般說明見I/O 系列文章,規模再大也可掛上 IOCP 或執行緒集區 I/O。

Overlapped 伺服器的結構各執行個體非同步操作的完成由事件陣列接收,少數執行緒以 WaitForMultipleObjects 等待並推進已完成的執行個體,使執行緒數與用戶端數脫鉤執行個體 1 非同步操作事件陣列執行個體 2 非同步操作執行個體 3 非同步操作以 WaitForMultipleObjects 等待完成推進已完成的執行個體

圖 5: Overlapped 形態讓執行緒數與用戶端數脫鉤。官方範例用單執行緒轉動這個循環。

.NET 幾乎消掉了這個選擇。NamedPipeServerStreamWaitForConnectionAsync / ReadAsync / WriteAsync 搭配 async/await,就能用和同步形態一樣直觀的程式碼得到 overlapped 的效率。9

// C#: skeleton of a server that accepts multiple clients
while (!token.IsCancellationRequested)
{
    var server = new NamedPipeServerStream(
        "MyCompany.MyApp.Control",
        PipeDirection.InOut,
        NamedPipeServerStream.MaxAllowedServerInstances,
        PipeTransmissionMode.Message,
        PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

    try
    {
        await server.WaitForConnectionAsync(token);
    }
    catch
    {
        await server.DisposeAsync();        // dispose yourself when leaving before a connection
        throw;
    }
    _ = HandleClientAsync(server, token);   // ownership after connect goes to the handler
}

PipeOptions.CurrentUserOnly 是「只允許同一使用者的處理程序連線」的指定,方便又安全的預設,省下自己寫 ACL。10 跨使用者的配置(服務 ↔ 使用者工作階段中的應用程式等)不能用它,那種情況就進入下一章的 ACL 設計。

.NET 非同步伺服器的接受迴圈接受迴圈建立 NamedPipeServerStream,以 WaitForConnectionAsync 等待連線,到達後非同步卸下用戶端處理並立刻回到下一次接受,因此並行連線可用直觀程式碼處理建立伺服器串流以 WaitForConnectionAsync 等待連線到達非同步卸下用戶端處理

圖 6: 接受迴圈只守「等待 → 卸下 → 下一個」這個循環,各用戶端的處理並行推進。

5. 安全性 ── 特權服務使用管道時必須做的四件事

具名管道成為同一機器 IPC 第一候補的最大理由是安全性模型,但那只在設定正確時成立。尤其在「系統管理員權限服務+低權限 UI 應用程式」的仲介設計裡,管道本身就是權限邊界。要釘死四點。

(1) 拒絕遠端。本意是本機 IPC 的管道卻能從網路開啟,本身就是攻擊面。在 CreateNamedPipe 指定 PIPE_REJECT_REMOTE_CLIENTS,遠端用戶端連線就會自動被拒絕。5

(2) 明確指定 ACL。SECURITY_ATTRIBUTES 傳入安全性描述元,縮小允許連線的使用者和群組。不要給用戶端 GENERIC_WRITE──其中包含的 FILE_CREATE_PIPE_INSTANCE 權限,會讓已授權的用戶端自己建立同名的伺服器執行個體並偷走後續連線。把讀與寫當成個別權限授予,不要傳入建立執行個體的權限。11

(3) 防止名稱劫持。管道名稱是先到先贏。惡意處理程序若先建立同名管道並等待,用戶端就會連上假伺服器。伺服器在建立第一個執行個體時指定 FILE_FLAG_FIRST_PIPE_INSTANCE,保證「我是第一個」,失敗就懷疑被劫持並停止。這個旗標只給宣告名稱的第一個執行個體;加在第二個以後的執行個體上會讓建立失敗。3

(4) 用戶端把偽裝層級降到所需的最小。這是為了對方是假伺服器的情況做準備。用戶端在 CreateFile 指定 **SECURITY_SQOS_PRESENT SECURITY_IDENTIFICATION,伺服器就能識別用戶端,但不能借用那些權限並行動2 不過這與偽裝工作流程是取捨──在伺服器要以用戶端權限實際存取的仲介設計中,識別層級不足以讓偽裝成功,必須允許 SECURITY_IMPERSONATION。那項允許的前提是確定連上的是真正的伺服器**。伺服器端的防劫持只是啟動失敗時會察覺的機制;若真正的服務不在、攻擊者先建立同名管道,用戶端仍可能連上假伺服器。只有在能透過保證的服務啟動、或連線後的相互驗證確認對方時,才允許它。

伺服器端的身分檢查與權限借用是 ImpersonateNamedPipeClient。從管道讀完要求後呼叫它,呼叫執行緒就開始以最後讀到的訊息傳送者的安全性脈絡執行。用用戶端權限開啟檔案,存取檢查就對用戶端進行──這就是特權服務執行「被要求的操作,但用要求者的權限」的機制。6 使用它的絕對條件是檢查傳回值。偽裝失敗後繼續,後續操作會以伺服器自身的高權限執行。官方文件明確寫道「失敗時不得執行用戶端的要求」。做完後的 RevertToSelf,以及偽裝權杖文章中的做法,原樣適用。

使用偽裝的要求處理流程伺服器從管道讀取要求,確認 ImpersonateNamedPipeClient 成功後,以用戶端權限執行操作,再用 RevertToSelf 回到自身脈絡。偽裝失敗則不執行並拒絕要求伺服器用戶端伺服器用戶端失敗時不執行並拒絕要求送出要求讀取要求ImpersonateNamedPipeClient以用戶端權限執行操作RevertToSelf 還原原本脈絡回覆結果

圖 7: 確認偽裝成功與確實的 RevertToSelf 是一套。失敗後繼續就會以伺服器權限執行。

保護特權服務管道的四點伺服器端以拒絕遠端、明確 ACL、第一個執行個體保證來加固入口;用戶端指定所需的最小偽裝層級,讓假伺服器無法借用權限(設計不讓伺服器借用權限時縮到識別層級)用戶端指定最小偽裝層級伺服器端PIPE_REJECT_REMOTE_CLIENTS以 ACL 限制連線者FIRST_PIPE_INSTANCE(僅第一個)作為權限邊界的管道

圖 8: 管道即權限邊界的設計中,伺服器端三點加上用戶端一點要成套實作。

6. 實務上的陷阱

啟動順序的競態。伺服器還沒建立管道,用戶端就來連線,會得到「管道不存在」的錯誤。用戶端端要內建「不存在 → 稍等再重試」。反過來說,伺服器端的原則是在用戶端啟動前就用 ConnectNamedPipe 開始聽。8

用戶端連線重試流程以 CreateFile 開啟管道;管道不存在就稍等再重試;所有執行個體都在使用中(ERROR_PIPE_BUSY)則用 WaitNamedPipe 等待空出後再重試;成功則進入通訊成功管道不存在ERROR_PIPE_BUSY以 CreateFile 開啟結果?開始通訊稍等(伺服器未啟動)以 WaitNamedPipe 等待空執行個體

圖 9: 用戶端連線處理區分「不存在」與「已滿」兩種失敗,兩者都接回重試。

偵測斷線。對方結束時,Read/Write 會以 ERROR_BROKEN_PIPE 等失敗。那不是異常,而是日常通訊。伺服器偵測斷線、對執行個體做 DisconnectNamedPipe、準備下一次連線;用戶端重新連線──睡眠/恢復文章所說「冪等重連」的想法在這裡也適用。

對訊息大小的假設。除了訊息模式的分割讀取(第 3 章)之外,若沒有把「一則訊息最多幾位元組」定進協定,惡意(或有缺陷)的對方就能用巨大訊息浪費你的記憶體。決定上限,超過就斷線;這才是安全做法。

寫入完成與對方收到是兩回事。WriteFile 成功不代表對方應用程式已處理資料。需要確定性的操作,要用回應訊息確認、並把要求與回應的對應寫進協定這類設計來擔保。

7. 總結

  • 具名管道是同一機器 IPC 的第一候補。理由是與檔案 I/O 相同的易用性,以及與 ACL、偽裝這套 Windows 安全性模型的整合。
  • 模式選擇是「要求與回應用訊息模式,已有自備框架就用位元組模式」。即使是訊息模式仍要處理分割讀取(ERROR_MORE_DATA)。
  • 多用戶端是多個執行個體+overlapped,或 .NET 的 async/await。新工作以 .NET 非同步形態最直觀。
  • 作為權限邊界的管道,要把拒絕遠端、明確 ACL、FIRST_PIPE_INSTANCE(僅第一個執行個體)、以及最小化用戶端偽裝層級當成一套。
  • ImpersonateNamedPipeClient 的生命線是檢查傳回值與 RevertToSelf
  • 把啟動順序、斷線、訊息上限、回應確認這些「通訊的日常」織進協定設計。

具名管道是舊 API,但對「讓同一台機器上的處理程序在尊重 Windows 帳號邊界的前提下交談」這個用途,它仍是最自然的工具。設計判斷點幾乎都收斂在本文範圍內。之後,先把自備協定寫成一張紙,再開始實作。

相關文章

相關諮詢領域

小村軟體有限公司承接涉及行程間通訊的設計與實作──把服務與 UI 應用程式分開、隔離系統管理員權限等──以及把既有 IPC(共享記憶體、自製通訊端、COM 等)換成具名管道,還有特權服務管道通訊的安全性審查。從一起討論協定設計開始諮詢也歡迎。

參考連結

  1. Microsoft Learn, Named Pipes. 關於具名管道是管道伺服器與一個或多個管道用戶端之間的單向或雙向通道;每個執行個體共用同一名稱卻有獨立緩衝區與控制代碼;以及可從本機與遠端處理程序使用。  2

  2. Microsoft Learn, Impersonating a Named Pipe Client. 關於偽裝讓伺服器執行緒在用戶端權限內操作;預設偽裝層級為 SecurityImpersonation;以及用戶端可在 CreateFile 時以 SECURITY_SQOS_PRESENT 旗標控制偽裝層級(SECURITY_IDENTIFICATION 只允許識別)。  2

  3. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). 關於管道方向(輸入、輸出、雙向)、位元組類型與訊息類型(PIPE_TYPE_BYTE / PIPE_TYPE_MESSAGE)及讀取模式(PIPE_READMODE_MESSAGE)、最大執行個體數(PIPE_UNLIMITED_INSTANCES)、以 FILE_FLAG_OVERLAPPED 啟用非同步模式、以 FILE_FLAG_FIRST_PIPE_INSTANCE 保證第一個執行個體,以及 WaitNamedPipe 的預設逾時。  2 3 4

  4. Microsoft Learn, Named Pipe Server Using Overlapped I/O. 關於以 overlapped 操作同時處理多個用戶端連線的單執行緒伺服器官方範例。以 WaitForMultipleObjects 等待各執行個體的 OVERLAPPED 結構與事件並推進已完成執行個體的狀態機,以及用 GetOverlappedResult 確認擱置 I/O 的完成。  2

  5. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). 關於兩種遠端用戶端模式:PIPE_ACCEPT_REMOTE_CLIENTS(接受遠端連線並依安全性描述元檢查)與 PIPE_REJECT_REMOTE_CLIENTS(自動拒絕遠端用戶端連線)。  2

  6. Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). 關於伺服器端執行緒開始以最後從管道讀到的訊息所屬用戶端的安全性脈絡進行偽裝;完成後以 RevertToSelf 返回;以及偽裝失敗後繼續會在伺服器處理程序自身(特權)脈絡中執行,因此必須一律檢查傳回值,失敗時不得執行用戶端的要求。  2 3

  7. Microsoft Learn, Named Pipe Client. 關於用戶端以 CreateFile 開啟管道;所有執行個體都在使用中時的 ERROR_PIPE_BUSY,以 WaitNamedPipe 等待空出;以及開啟的控制代碼預設為位元組讀取、阻塞、非 overlapped,可用 SetNamedPipeHandleState 改成訊息讀取模式。  2

  8. Microsoft Learn, Named Pipe Operations. 關於以 ReadFileEx / WriteFileEx 做 overlapped 操作、以 PeekNamedPipe 做不消耗的讀取、TransactNamedPipe 在訊息類型雙向管道上一次完成要求送出與回應接收,以及用戶端啟動前的阻塞讀取可能造成競態。  2

  9. Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). 關於以 NamedPipeServerStream / NamedPipeClientStream 連線與讀寫、以 PipeTransmissionMode.Message 做訊息單位傳輸,以及用非同步方法處理多個用戶端。  2

  10. Microsoft Learn, PipeOptions Enum (System.IO.Pipes). 關於以 Asynchronous 啟用非同步 I/O,以及 CurrentUserOnly 能只允許與同一使用者(且同一提升層級)的處理程序連線。 

  11. Microsoft Learn, Named Pipe Security and Access Rights. 關於具名管道存取權的組成;GENERIC_WRITE 包含 FILE_CREATE_PIPE_INSTANCE,因此給用戶端一般寫入也等於允許建立伺服器執行個體;以及資料的讀與寫應以個別存取權授予。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

具名管道與 TCP(localhost 通訊端)該怎麼選?
同一台機器上的行程間通訊,具名管道是第一候補。理由在安全性模型。管道可用 Windows 安全性描述元(ACL)在作業系統層級控制「誰可以連線」,伺服器也能用 ImpersonateNamedPipeClient 檢查並借用連線對方的 Windows 帳號。相對地,localhost 的 TCP 連接埠任何人都能連上,必須靠自備的驗證才能確認對方是誰。另一方面,若日後很可能發展成遠端通訊、也要和其他作業系統上的處理程序交談,或想沿用 gRPC 這類既有協定資產,TCP 為基礎的選項較有利。這項判斷也整理在 Windows 行程間通訊選擇的文章裡。
該用位元組模式還是訊息模式?
若想把「一次寫入=一個意義單位」來處理,訊息模式(PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE)比較方便。接收端能依傳送端寫入的單位讀取,不必自己管理邊界。位元組模式是像 TCP 那樣的「沒有斷點的位元組串流」,必須自己設計框架(例如長度前置碼)。若承載的協定本身已有框架(例如帶長度的序列化形式),用位元組模式即可。要注意:即使是訊息模式,接收緩衝區比訊息小時仍會發生分割讀取(ERROR_MORE_DATA),因此還是要處理。此外,讀取模式是每個控制代碼各自的設定,CreateNamedPipe 只設定伺服器端。用戶端必須在 CreateFile 之後用 SetNamedPipeHandleState 指定 PIPE_READMODE_MESSAGE。在 .NET 中,伺服器指定 PipeTransmissionMode.Message,用戶端則在連線後把 NamedPipeClientStream.ReadMode 設為 Message。
要怎麼做一個同時和多個用戶端交談的伺服器?
具名管道可在同一個名稱下建立多個執行個體,一個執行個體對應一個用戶端。形態有兩種。一種是每個用戶端分配一條執行緒的同步設計,實作直觀,但每個用戶端都會消耗一條執行緒。另一種是用 FILE_FLAG_OVERLAPPED 做非同步 I/O,由少數執行緒處理所有執行個體的 ConnectNamedPipe、ReadFile、WriteFile;Microsoft 的官方範例也示範了以單執行緒處理多個執行個體的實作。在 .NET 可用 NamedPipeServerStream.WaitForConnectionAsync 搭配 async/await,用幾乎和同步一樣直觀的寫法完成非同步形態。除非有特別理由,新實作我建議採用 .NET 的非同步形態。
具名管道安全性最少該做哪些事?
四點。第一,若不需要遠端連線,指定 PIPE_REJECT_REMOTE_CLIENTS,明確拒絕經由網路的連線。第二,用 SECURITY_ATTRIBUTES 設定適當的 ACL,縮小可連線的使用者和群組(預設 ACL 對某些用途過寬)。第三,建立第一個執行個體時指定 FILE_FLAG_FIRST_PIPE_INSTANCE,以便偵測「同名管道被搶先建立」的名稱劫持(第二個以後的執行個體不要加這個旗標)。第四,只希望伺服器識別自己的用戶端,應在 CreateFile 指定 SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION,讓假伺服器無法借用(偽裝)自己的權限。在伺服器要以用戶端權限實際存取的仲介(broker)設計中,必須允許偽裝,因此這項限制要依「設計是否讓伺服器借用權限」來取捨。
使用 ImpersonateNamedPipeClient 有什麼要注意?
最重要的是檢查傳回值。偽裝失敗後若繼續處理,後續操作會以伺服器處理程序自身(往往較高)的權限執行,本不該允許用戶端做的操作就會通過。官方文件也明確寫道:失敗時不得執行用戶端的要求。此外必須先讀過某些內容再呼叫──偽裝是在「最後從管道讀到的訊息」這個脈絡下進行──做完後要以 RevertToSelf 確實回到原本的脈絡。偽裝周邊的機制(權杖、偽裝層級、SeImpersonatePrivilege)在偽裝權杖的文章裡有詳細說明。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽