FileZilla Server源碼分析(3)
Posted on 2010-06-10 13:38 亂78糟 閱讀(2337) 評(píng)論(1) 編輯 收藏 引用 所屬分類(lèi): 開(kāi)源這是分析的第三節(jié),上一節(jié)主要講了一些和socket基礎(chǔ)操作相關(guān)的代碼,本節(jié)將分析核心代碼。
Service.cpp 系統(tǒng)服務(wù)程序
FileZillaServer可以選擇是否注冊(cè)成windows的服務(wù)程序,而這個(gè)服務(wù)程序的代碼就是由service.cpp文件實(shí)現(xiàn)的。
WinMain是它的入口函數(shù),在WinMain里依次完成了下面幾項(xiàng)任務(wù):
serviceMain注冊(cè)Handler成功之后,就啟動(dòng)自己的工作線(xiàn)程ServiceExecutionThread,線(xiàn)程里創(chuàng)建了CServer對(duì)象,然后實(shí)際流程交由CServer。之后進(jìn)入線(xiàn)程的消息循環(huán)并等待killServiceEvent信號(hào)以退出線(xiàn)程終止服務(wù)。
Service.cpp中KillService函數(shù)中有一個(gè)變量hMainWnd,它是在stdafx.h中聲明的,它具體是哪個(gè)窗口的句柄,干什么用,現(xiàn)在還是一無(wú)所知。
Server.* 真正的帶頭大哥
打開(kāi)Server.h文件,開(kāi)頭就可以看到許多類(lèi)的前置聲明,類(lèi)中聲明了眾多上一節(jié)提到的相關(guān)類(lèi)對(duì)象(或集合如list),CServer類(lèi)把所有核心類(lèi)(線(xiàn)程和socket)集中起來(lái)使用。
上面已經(jīng)提到Service的工作線(xiàn)程中調(diào)用了CServer的Create函數(shù),我們就先從這里入手吧。
Create一開(kāi)始就創(chuàng)建了一個(gè)窗口,標(biāo)題為"FileZilla Server Helper Window",呵呵,是不是很熟悉?然后將這個(gè)窗口的句柄賦給全局變量hMainWnd,至此,終于大致了解了服務(wù)的框架骨骼。
之后又是一大堆初始化操作,包括兩個(gè)定時(shí)器,需要特別注意的是創(chuàng)建服務(wù)線(xiàn)程CServerThread時(shí)候的提供的參數(shù)WM_FILEZILLA_SERVERMSG + index,這個(gè)參數(shù)用以線(xiàn)程間通信,再上一節(jié)已經(jīng)有過(guò)描述,稍后再具體分析。
在往下調(diào)用了CreateListenSocket函數(shù),這個(gè)函數(shù)根據(jù)Options類(lèi)中獲取的port、bindip、enablessl等參數(shù)創(chuàng)建監(jiān)聽(tīng)ftp客戶(hù)端連接的CListenSocket對(duì)象指針,并保存到m_ListenSocketList中。這里有一個(gè)很重要的函數(shù)ShowStatus,它的任務(wù)是將信息發(fā)送給admin窗口和記錄到log中。
最后調(diào)用CreateAdminListenSocket函數(shù)創(chuàng)建監(jiān)聽(tīng)admin客戶(hù)端的socket,并存入m_AdminListenSocketList中。
CServer類(lèi)的分析暫時(shí)中斷一下,我們來(lái)分析上面涉及到的幾個(gè)相關(guān)類(lèi):CServerThread,CListenSocket,CControlSocket,CTransferSocket。
CServerThread繼承自CThread,構(gòu)造函數(shù)有個(gè)int型參數(shù),用來(lái)標(biāo)識(shí)具體哪個(gè)線(xiàn)程的消息。注意,CThread本身并不是一個(gè)直接繼承于任何線(xiàn)程類(lèi)的類(lèi),它只是負(fù)責(zé)創(chuàng)建并管理線(xiàn)程的類(lèi)。m_sInstanceList是static的成員變量,被所有的CServerThread對(duì)象共享,而且這個(gè)list存儲(chǔ)的第一個(gè)值用于管理SL,為了標(biāo)識(shí)它,作者又添加了一個(gè)BOOL成員變量m_bIsMaster,同樣還有一個(gè)static臨界區(qū)變量m_GlobalThreadsync用來(lái)同步它。如果當(dāng)前對(duì)象是master,那么它還擁有一個(gè)用于實(shí)現(xiàn)PASV模式的CExternalIpCheck的類(lèi)對(duì)象m_pExternalIpCheck,缺省值是不采用PASV的。
對(duì)CServerThread的重要的幾個(gè)Public成員函數(shù)分析一下功能:
對(duì)CServerThread重要的幾個(gè)非public成員函數(shù)分析一下功能:
CListenSocket類(lèi)功能很簡(jiǎn)單,如果一個(gè)連接被accept,那么從服務(wù)線(xiàn)程CServerThread列表中找到負(fù)載最小的線(xiàn)程,然后調(diào)用的AddSocket函數(shù),將這個(gè)連接交給這個(gè)CServerTread管理。
CControlSocket類(lèi)負(fù)責(zé)與客戶(hù)端交互。
它有一個(gè)int型成員變量m_antiHammeringWaitTime,用來(lái)防止用戶(hù)攻擊(即無(wú)限次嘗試登錄),例如某用戶(hù)在60秒內(nèi)連續(xù)嘗試登錄10次失敗,那么就把這個(gè)用戶(hù)加入ban列表中,比如3000秒內(nèi)拒絕再次登錄等。AntiHammerIncrease 函數(shù)中對(duì)這個(gè)變量的算法沒(méi)看明白
:
PassCommand函數(shù)處理所有的命令,如USER、LIST、PASV、STOR等。當(dāng)收到STOR命令時(shí),如果是PASV模式,那么調(diào)用m_transferstatus.socket->PasvTransfer(),否則新建一個(gè)CTransferSocket套接字賦給m_transferstatus.socket,然后調(diào)用SendTransferinfoNotification發(fā)送TRANSFERMODE_RECEIVE消息。不管哪種方式,最后還是通過(guò)調(diào)用CTransferSocket的InitTransfer函數(shù)實(shí)現(xiàn)文件傳輸。
好了,現(xiàn)在讓我們恢復(fù)現(xiàn)場(chǎng)。
CServer類(lèi)的消息處理函數(shù)WindowProc,處理了各種消息,其中重要的是WM_DESTROY和WM_FILEZILLA_SERVERMSG。前者通知并等待所有線(xiàn)程退出,關(guān)閉socket,銷(xiāo)毀資源,殺死定時(shí)器,做的都是清理工作。后者根據(jù)服務(wù)線(xiàn)程發(fā)送來(lái)的消息進(jìn)入函數(shù)OnServerMessage中,這個(gè)函數(shù)處理了所有服務(wù)管理的消息。可以看到,很多消息最后都是通過(guò)m_pAdminInterface->SendCommand(2, 3, buffer, len)這句發(fā)送出去。CAdminInterface類(lèi)管理CAdminSocket類(lèi)的指針列表,SendCommand其實(shí)是調(diào)用CAdminSocket的SendCommand將消息發(fā)送出去。函數(shù)中對(duì)admin socket做了自動(dòng)管理,如果操作失敗,就自動(dòng)移除該socket。
CheckForTimeout每10秒由CServer的定時(shí)器調(diào)用一次,檢測(cè)admin socket是否超時(shí),如果超時(shí),自動(dòng)移除。CAdminSocket收到數(shù)據(jù)并解析成功之后,最終交由CServer的ProcessCommand處理,該函數(shù)再一次根據(jù)Options里的設(shè)置對(duì)線(xiàn)程、socket進(jìn)行一次校驗(yàn)和調(diào)整。
我個(gè)人對(duì)ProcessCommand和SendCommand函數(shù)參數(shù)中的type或nID為int型有微議,因?yàn)檫@兩個(gè)參數(shù)實(shí)際只占用了不到8個(gè)字節(jié),寫(xiě)為int不利于理解,如果改成int8一眼就能看出來(lái)這個(gè)參數(shù)具體占用幾個(gè)字節(jié)。
下面重點(diǎn)分析一下ProcessCommand這個(gè)函數(shù),用偽代碼比較直觀(guān)。
這一節(jié)涵蓋了眾多核心代碼,上面的分析相對(duì)來(lái)說(shuō)還是比較粗略,所以,后面幾節(jié)在對(duì)這些粗略和遺漏部分在做更為詳細(xì)深入的挖掘,本節(jié)到這里就結(jié)束了。
因?yàn)槎际强创a時(shí)臨時(shí)寫(xiě)入筆記的,所有的分析都很雜亂,希望以后我有時(shí)間可以畫(huà)一些圖,重新做一次整理。
2010-7-22補(bǔ)充
圖隨便畫(huà)了幾張,鏈接在此
PS: 本來(lái)上周就可以貼出來(lái)了,可是因?yàn)榘惭bMAC系統(tǒng)造成C盤(pán)WINDOWS系統(tǒng)數(shù)據(jù)破壞無(wú)法啟動(dòng),重裝系統(tǒng)導(dǎo)致筆記丟失,這里只能補(bǔ)上,拖后了一周左右。
Service.cpp 系統(tǒng)服務(wù)程序
FileZillaServer可以選擇是否注冊(cè)成windows的服務(wù)程序,而這個(gè)服務(wù)程序的代碼就是由service.cpp文件實(shí)現(xiàn)的。
WinMain是它的入口函數(shù),在WinMain里依次完成了下面幾項(xiàng)任務(wù):
- 參數(shù)解析
- 初始化某些數(shù)據(jù)比如端口
- 由SCM(服務(wù)控制管理器)啟動(dòng)服務(wù),入口為ServiceMain函數(shù);如果服務(wù)不存在,進(jìn)入步驟4
- 根據(jù)參數(shù)設(shè)置服務(wù),例如安裝、啟動(dòng)、卸載等。
serviceMain注冊(cè)Handler成功之后,就啟動(dòng)自己的工作線(xiàn)程ServiceExecutionThread,線(xiàn)程里創(chuàng)建了CServer對(duì)象,然后實(shí)際流程交由CServer。之后進(jìn)入線(xiàn)程的消息循環(huán)并等待killServiceEvent信號(hào)以退出線(xiàn)程終止服務(wù)。
Service.cpp中KillService函數(shù)中有一個(gè)變量hMainWnd,它是在stdafx.h中聲明的,它具體是哪個(gè)窗口的句柄,干什么用,現(xiàn)在還是一無(wú)所知。
Server.* 真正的帶頭大哥
打開(kāi)Server.h文件,開(kāi)頭就可以看到許多類(lèi)的前置聲明,類(lèi)中聲明了眾多上一節(jié)提到的相關(guān)類(lèi)對(duì)象(或集合如list),CServer類(lèi)把所有核心類(lèi)(線(xiàn)程和socket)集中起來(lái)使用。
上面已經(jīng)提到Service的工作線(xiàn)程中調(diào)用了CServer的Create函數(shù),我們就先從這里入手吧。
Create一開(kāi)始就創(chuàng)建了一個(gè)窗口,標(biāo)題為"FileZilla Server Helper Window",呵呵,是不是很熟悉?然后將這個(gè)窗口的句柄賦給全局變量hMainWnd,至此,終于大致了解了服務(wù)的框架骨骼。
之后又是一大堆初始化操作,包括兩個(gè)定時(shí)器,需要特別注意的是創(chuàng)建服務(wù)線(xiàn)程CServerThread時(shí)候的提供的參數(shù)WM_FILEZILLA_SERVERMSG + index,這個(gè)參數(shù)用以線(xiàn)程間通信,再上一節(jié)已經(jīng)有過(guò)描述,稍后再具體分析。
在往下調(diào)用了CreateListenSocket函數(shù),這個(gè)函數(shù)根據(jù)Options類(lèi)中獲取的port、bindip、enablessl等參數(shù)創(chuàng)建監(jiān)聽(tīng)ftp客戶(hù)端連接的CListenSocket對(duì)象指針,并保存到m_ListenSocketList中。這里有一個(gè)很重要的函數(shù)ShowStatus,它的任務(wù)是將信息發(fā)送給admin窗口和記錄到log中。
最后調(diào)用CreateAdminListenSocket函數(shù)創(chuàng)建監(jiān)聽(tīng)admin客戶(hù)端的socket,并存入m_AdminListenSocketList中。
CServer類(lèi)的分析暫時(shí)中斷一下,我們來(lái)分析上面涉及到的幾個(gè)相關(guān)類(lèi):CServerThread,CListenSocket,CControlSocket,CTransferSocket。
CServerThread繼承自CThread,構(gòu)造函數(shù)有個(gè)int型參數(shù),用來(lái)標(biāo)識(shí)具體哪個(gè)線(xiàn)程的消息。注意,CThread本身并不是一個(gè)直接繼承于任何線(xiàn)程類(lèi)的類(lèi),它只是負(fù)責(zé)創(chuàng)建并管理線(xiàn)程的類(lèi)。m_sInstanceList是static的成員變量,被所有的CServerThread對(duì)象共享,而且這個(gè)list存儲(chǔ)的第一個(gè)值用于管理SL,為了標(biāo)識(shí)它,作者又添加了一個(gè)BOOL成員變量m_bIsMaster,同樣還有一個(gè)static臨界區(qū)變量m_GlobalThreadsync用來(lái)同步它。如果當(dāng)前對(duì)象是master,那么它還擁有一個(gè)用于實(shí)現(xiàn)PASV模式的CExternalIpCheck的類(lèi)對(duì)象m_pExternalIpCheck,缺省值是不采用PASV的。
對(duì)CServerThread的重要的幾個(gè)Public成員函數(shù)分析一下功能:
- GetExternalIP : 調(diào)用m_pExternalIpCheck獲取PASV的ip
- AddSocket:給自己發(fā)送一個(gè)線(xiàn)程消息,該消息在OnThreadMessage函數(shù)中被處理,用來(lái)添加(SSL)socket連接
對(duì)CServerThread重要的幾個(gè)非public成員函數(shù)分析一下功能:
- AddNewSocket:將sokcet handle綁定到新new的CControlSocket對(duì)象socket上,并為當(dāng)前socket分配一個(gè)唯一的用戶(hù)ID。分配函數(shù)CalcUserID不算高效,尤其是連接用戶(hù)數(shù)量比較大的時(shí)候再分配尤其明顯。之后調(diào)用SendNotification準(zhǔn)備發(fā)送包含連接用戶(hù)的信息的消息給CServer,最后向連接的用戶(hù)發(fā)送歡迎信息。
- SendNotification:這個(gè)函數(shù)將需要發(fā)送的數(shù)據(jù)加入待發(fā)送list中,最牛的是它可以自動(dòng)調(diào)節(jié)發(fā)送的效率。不過(guò)我發(fā)現(xiàn)一處小BUG,可能作者自己也沒(méi)有注意到,這兩處設(shè)置線(xiàn)程優(yōu)先級(jí)貌似反了:
else if (m_pendingNotifications.size() > 150 && m_throttled < 2)
{
SetPriority(THREAD_PRIORITY_LOWEST);
m_throttled = 2;
}
else if (m_pendingNotifications.size() > 100 && !m_throttled)
{
SetPriority(THREAD_PRIORITY_BELOW_NORMAL);
m_throttled = 1;
}
{
SetPriority(THREAD_PRIORITY_LOWEST);
m_throttled = 2;
}
else if (m_pendingNotifications.size() > 100 && !m_throttled)
{
SetPriority(THREAD_PRIORITY_BELOW_NORMAL);
m_throttled = 1;
}
- OnThreadMessage:線(xiàn)程消息處理函數(shù),如添加刪除用戶(hù),解析命令,傳輸,控制,計(jì)時(shí)器等。
CListenSocket類(lèi)功能很簡(jiǎn)單,如果一個(gè)連接被accept,那么從服務(wù)線(xiàn)程CServerThread列表中找到負(fù)載最小的線(xiàn)程,然后調(diào)用的AddSocket函數(shù),將這個(gè)連接交給這個(gè)CServerTread管理。
CControlSocket類(lèi)負(fù)責(zé)與客戶(hù)端交互。
它有一個(gè)int型成員變量m_antiHammeringWaitTime,用來(lái)防止用戶(hù)攻擊(即無(wú)限次嘗試登錄),例如某用戶(hù)在60秒內(nèi)連續(xù)嘗試登錄10次失敗,那么就把這個(gè)用戶(hù)加入ban列表中,比如3000秒內(nèi)拒絕再次登錄等。AntiHammerIncrease 函數(shù)中對(duì)這個(gè)變量的算法沒(méi)看明白

if (m_status.hammerValue > 2000)
m_antiHammeringWaitTime += 1000 * (int)pow(1.3, (m_status.hammerValue / 400) - 5);
在用戶(hù)登錄的時(shí)候就去檢測(cè)是否是“攻擊”,代碼如下:m_antiHammeringWaitTime += 1000 * (int)pow(1.3, (m_status.hammerValue / 400) - 5);
BOOL bResult = GetPeerName((SOCKADDR*)&sockAddr, &nSockAddrLen);
if (bResult)
m_pOwner->AntiHammerIncrease(sockAddr.sin_addr.s_addr);
if (m_pOwner->m_pAutoBanManager->RegisterAttempt(htonl(sockAddr.sin_addr.s_addr)))
{
Send(_T("421 Temporarily banned for too many failed login attempts"));
ForceClose(-1);
return FALSE;
}
if (bResult)
m_pOwner->AntiHammerIncrease(sockAddr.sin_addr.s_addr);
if (m_pOwner->m_pAutoBanManager->RegisterAttempt(htonl(sockAddr.sin_addr.s_addr)))
{
Send(_T("421 Temporarily banned for too many failed login attempts"));
ForceClose(-1);
return FALSE;
}
PassCommand函數(shù)處理所有的命令,如USER、LIST、PASV、STOR等。當(dāng)收到STOR命令時(shí),如果是PASV模式,那么調(diào)用m_transferstatus.socket->PasvTransfer(),否則新建一個(gè)CTransferSocket套接字賦給m_transferstatus.socket,然后調(diào)用SendTransferinfoNotification發(fā)送TRANSFERMODE_RECEIVE消息。不管哪種方式,最后還是通過(guò)調(diào)用CTransferSocket的InitTransfer函數(shù)實(shí)現(xiàn)文件傳輸。
好了,現(xiàn)在讓我們恢復(fù)現(xiàn)場(chǎng)。
CServer類(lèi)的消息處理函數(shù)WindowProc,處理了各種消息,其中重要的是WM_DESTROY和WM_FILEZILLA_SERVERMSG。前者通知并等待所有線(xiàn)程退出,關(guān)閉socket,銷(xiāo)毀資源,殺死定時(shí)器,做的都是清理工作。后者根據(jù)服務(wù)線(xiàn)程發(fā)送來(lái)的消息進(jìn)入函數(shù)OnServerMessage中,這個(gè)函數(shù)處理了所有服務(wù)管理的消息。可以看到,很多消息最后都是通過(guò)m_pAdminInterface->SendCommand(2, 3, buffer, len)這句發(fā)送出去。CAdminInterface類(lèi)管理CAdminSocket類(lèi)的指針列表,SendCommand其實(shí)是調(diào)用CAdminSocket的SendCommand將消息發(fā)送出去。函數(shù)中對(duì)admin socket做了自動(dòng)管理,如果操作失敗,就自動(dòng)移除該socket。
CheckForTimeout每10秒由CServer的定時(shí)器調(diào)用一次,檢測(cè)admin socket是否超時(shí),如果超時(shí),自動(dòng)移除。CAdminSocket收到數(shù)據(jù)并解析成功之后,最終交由CServer的ProcessCommand處理,該函數(shù)再一次根據(jù)Options里的設(shè)置對(duì)線(xiàn)程、socket進(jìn)行一次校驗(yàn)和調(diào)整。
我個(gè)人對(duì)ProcessCommand和SendCommand函數(shù)參數(shù)中的type或nID為int型有微議,因?yàn)檫@兩個(gè)參數(shù)實(shí)際只占用了不到8個(gè)字節(jié),寫(xiě)為int不利于理解,如果改成int8一眼就能看出來(lái)這個(gè)參數(shù)具體占用幾個(gè)字節(jié)。
BOOL CAdminSocket::SendCommand(int nType, int nID, const void *pData, int nDataLength)
{
/*
*/
t_data data;
data.pData = new unsigned char[nDataLength + 5];
*data.pData = nType; //nType目前版本只要不為0就是合法的協(xié)議類(lèi)型,代碼中用到了1和2
*data.pData |= nID << 2; //nType和nID合用一個(gè)8字節(jié)
data.dwOffset = 0;
memcpy(data.pData + 1, &nDataLength, 4);
/*
*/
}
{
/*

t_data data;
data.pData = new unsigned char[nDataLength + 5];
*data.pData = nType; //nType目前版本只要不為0就是合法的協(xié)議類(lèi)型,代碼中用到了1和2
*data.pData |= nID << 2; //nType和nID合用一個(gè)8字節(jié)
data.dwOffset = 0;
memcpy(data.pData + 1, &nDataLength, 4);
/*

}
下面重點(diǎn)分析一下ProcessCommand這個(gè)函數(shù),用偽代碼比較直觀(guān)。
BOOL CServer::ProcessCommand(CAdminSocket *pAdminSocket, int nID, unsigned char *pData, int nDataLength)
{
switch(nID)
{
case 2:
if (!nDataLength)
//獲取服務(wù)器狀態(tài)
else
//設(shè)置服務(wù)器狀態(tài)并獲取
else
//send error:wrong protocol type
break;
case 3:
if (!nDataLength)
//send error
else if (*pData == USERCONTROL_GETLIST)
//計(jì)算并格式化所有已連接用戶(hù)的信息到unsigned char *buffer中并發(fā)送給admin
//這些數(shù)據(jù)顯示在admin UI下方的user list中
else if (*pData == USERCONTROL_KICK || *pData == USERCONTROL_BAN)
//*pData共5個(gè)字節(jié),第一個(gè)為具體協(xié)議類(lèi)型,后四個(gè)為userID。
//根據(jù)協(xié)議對(duì)userID進(jìn)行操作,kick或者Ban掉。
else
//send error: wrong protocol type
break;
case 5:
if (!nDataLength)
//讀取基本配置然后發(fā)送給admin
else if (*m_pOptions)
//解析配置字符串,創(chuàng)建初始化或調(diào)整CServerThread
//CreateListenSocket
//創(chuàng)建admin監(jiān)聽(tīng)sockets
break;
case 6:
if (!nDataLength)
//讀取user和group的權(quán)限配置
else
//解析權(quán)限配置發(fā)送給admin
break;
case 8:
pAdminSocket->SendCommand(1, 8, NULL, 0);
break;
default:
//send error: unknow command
}
return true;
}
{
switch(nID)
{
case 2:
if (!nDataLength)
//獲取服務(wù)器狀態(tài)
else
//設(shè)置服務(wù)器狀態(tài)并獲取
else
//send error:wrong protocol type
break;
case 3:
if (!nDataLength)
//send error
else if (*pData == USERCONTROL_GETLIST)
//計(jì)算并格式化所有已連接用戶(hù)的信息到unsigned char *buffer中并發(fā)送給admin
//這些數(shù)據(jù)顯示在admin UI下方的user list中
else if (*pData == USERCONTROL_KICK || *pData == USERCONTROL_BAN)
//*pData共5個(gè)字節(jié),第一個(gè)為具體協(xié)議類(lèi)型,后四個(gè)為userID。
//根據(jù)協(xié)議對(duì)userID進(jìn)行操作,kick或者Ban掉。
else
//send error: wrong protocol type
break;
case 5:
if (!nDataLength)
//讀取基本配置然后發(fā)送給admin
else if (*m_pOptions)
//解析配置字符串,創(chuàng)建初始化或調(diào)整CServerThread
//CreateListenSocket
//創(chuàng)建admin監(jiān)聽(tīng)sockets
break;
case 6:
if (!nDataLength)
//讀取user和group的權(quán)限配置
else
//解析權(quán)限配置發(fā)送給admin
break;
case 8:
pAdminSocket->SendCommand(1, 8, NULL, 0);
break;
default:
//send error: unknow command
}
return true;
}
這一節(jié)涵蓋了眾多核心代碼,上面的分析相對(duì)來(lái)說(shuō)還是比較粗略,所以,后面幾節(jié)在對(duì)這些粗略和遺漏部分在做更為詳細(xì)深入的挖掘,本節(jié)到這里就結(jié)束了。
因?yàn)槎际强创a時(shí)臨時(shí)寫(xiě)入筆記的,所有的分析都很雜亂,希望以后我有時(shí)間可以畫(huà)一些圖,重新做一次整理。
2010-7-22補(bǔ)充
圖隨便畫(huà)了幾張,鏈接在此
PS: 本來(lái)上周就可以貼出來(lái)了,可是因?yàn)榘惭bMAC系統(tǒng)造成C盤(pán)WINDOWS系統(tǒng)數(shù)據(jù)破壞無(wú)法啟動(dòng),重裝系統(tǒng)導(dǎo)致筆記丟失,這里只能補(bǔ)上,拖后了一周左右。