標籤:blog and += bfd 測試程式 rsa 四種 驅動 產生
學習目標
上一節我們瞭解了進程、入口函數和進程執行個體控制代碼等內容,在進入進程的命令列學習前,有一個全域變數初始化問題需要測試一波。本節的學習目標如下:
1.測試C/C++運行庫啟動函數初始化哪些全域變數
2.進程的命令列
3.進程的環境變數
4.進程的當前磁碟機和目錄
5.判斷系統版本
6.建立進程(CreateProcess函數詳解)
測試啟動函數初始化哪些全域變數
我們知道C/C++運行庫的啟動函數會做一些事後再調用我們的入口函數,而入口函數的參數都是在調用前就初始化好了的。那麼我就產生了一個疑問,全域變數隨入口函數的不同(四種入口函數,分別是main、wmain、wWinMain、WinMain)都分別初始化了哪些全域變數?我做出了下面的測試:
(1)在CUI程式下測試Unicode字元集和多位元組字元集兩種情況的全域變數的初始化:
#include<windows.h>#include<tchar.h>#include<iostream>using namespace std;int _tmain(int argc, _TCHAR* argv[]){ //測試兩次:第一次是在Unicode環境下,第二次是在多位元組字元集環境下,注意輸出的不同。 //測試_environ有無被初始化成有用的值 char** p1 = _environ; if (p1 != NULL) { while (*p1) { cout << *p1 << endl; p1++; } cout << "---------------------------上面的是_environ輸出的值---------------------------------" << endl; } //測試_wenviron有無被初始化成有用的值 wchar_t** p2 = _wenviron; if (p2 != NULL) { while (*p2) { wcout << *p2 << endl; p2++; } cout << "--------------------------上面的是_wenviron輸出的值--------------------------" << endl; } //測試__argv有無被初始化成有用的值 char** p3= __argv; if (p3 != NULL) { while (*p3) { cout << *p3 << endl; p3++; } cout << "-------------------------上面的是__argv輸出的值----------------------------" << endl; } //測試__wargv有無被初始化成有用的值 wchar_t** p4 = __wargv; if (p4 != NULL) { while (*p4) { wcout << *p4 << endl; p4++; } cout << "-------------------------上面的是__wargv輸出的值----------------------------" << endl; } system("pause"); return 0;}
測試結果:輸出結果太長不好,這裡只給出總結,運行結果可以自己運行查看。如果你寫的主函數是_tmain,那麼其中_environ和_wenviron全域變數,在Unicode環境下,_environ和_wenviron全域變數都被初始化成有用的值了。而在多位元組字元集下,_environ全域變數被初始化成有用的值,_wenviron全域變數才被置NULL。明顯,和書中P69頁表格的描述有差異
(2)在GUI程式下測試Unicode字元集和多位元組字元集兩種情況的全域變數的初始化:
1.很明顯,下面的測試結果和在wmain函數測試情況相同。
2.可以看出,下面的測試結果和在main函數測試情況也相同。
(3)大總結:
對於書中P69頁的全域變數初始化表的描述我產生了質疑。如果你寫的主函數是_tmain,那麼其中_environ和_wenviron全域變數,在Unicode環境下,_environ和_wenviron全域變數都被初始化成有用的值了。而在多位元組字元集下,_environ全域變數被初始化成有用的值,_wenviron全域變數才被置NULL。簡單來說就是無論_UNICODE有無被定義,_environ都會被初始化成有用的值,而_wenviron就受字元集影響,跟書產生了歧義。而wargv和argv就是符合書本的情況定義。如果你寫的主函數是_tWinMain,那麼其中_environ和_wenviron全域變數,在Unicode環境下,_environ和_wenviron全域變數都被初始化成有用的值了。而在多位元組字元集下,_environ全域變數被初始化成有用的值,_wenviron全域變數才被置NULL。而wargv和argv就是符合書本的情況定義。注意,如果Windows編程,不使用_tmain和_tWinMain函數,而是使用main或wmain,那麼上述的總結不一定成立,但由於兼顧兩種字元集,建議以後寫的入口函數就寫_tmain和_tWinMain函數。
進程的命令列
(1)如果是運行CUI應用程式,在C/C++運行庫啟動函數執行時,就已經初始化好全域變數(包括命令列參數argc、argv或wargv。如果在Unicode字元集下,初始化了argc、argv;如果在多字元集下,初始化了argc、__wargv。)然後調用進入點函數_tmain,將參數argc、argv或wargv傳入_tmain函數。
現在對_tmain函數的參數進行測試:
#include<windows.h>#include<tchar.h>#include<iostream>using namespace std;int _tmain(int argc, _TCHAR* argv[]){ /* 有兩種方式可以輸入命令列參數: 1.屬性->配置屬性->調試->命令參數:例如:wo ai ni 2.在可執行檔目錄下開啟命令列視窗(cmd),輸入檔案名稱+命令列參數:例如:ConsoleApplication9 wo ai ni 但有一點需要注意,就是字元集問題,當項目字元集是Unicode字元集,那麼在C++利用wcout輸出命令列。當項目字元集是多位元組字元集,那麼在C++利用cout輸出命令列。 注意,不論通過以上兩種方式輸入的命令列參數都會在C/C++運行庫啟動函數中被初始化全域變數argc、__argv、__wargv。 所以傳入_tmain函數的argv參數也是對應字元集編碼的字串。例如:如果在Unicode下,argv數組內的元素就是寬字元串,如果在多位元組字元集下,argv數組內的元素就是ANSI字串。 注意第一種方式和第二種方式在輸出上的區別,第一種輸出的第一個檔案名稱字串,這個字串也包括路徑。而第二種輸出只有命令列參數,因為就算沒有填寫命令列參數也會輸出檔案名,那個檔案名稱 只是起到運行這個程式的象徵。 */ for (int i = 0; i < argc; i++) { //cout只能輸出ANSI字元和字串,要想輸出寬字元可以使用wcout。 wcout << argv[i] << endl; } system("pause"); return 0;}
(2)如果是運行CUI應用程式,在C/C++運行庫啟動函數執行時,會調用Windows函數GetCommandLine來擷取進程的完整命令列(檔案名稱+命令列參數,其中檔案名稱也就是絕對路徑)然後啟動函數進行忽略可執行檔的名稱,包括路徑,接著將指向命令列剩餘部分的一個指標傳給WinMain的pszCmdLine參數。下面給出函數的簽名:
LPTSTR WINAPI GetCommandLine(void);
下面舉個例子:
可以看出cmdLine包含絕對路徑的檔案名稱和命令列參數。而pszCmdLine參數只有命令列參數,因為在啟動函數處理中已經忽略了檔案名稱了。
(3)我們也可以利用CommandLinetoArgvW函數將GetCommandLineW函數擷取的完整命令列分解成單獨的標記。
該函數原型如下:
LPWSTR* CommandLinetoArgvW(LPCWSTR,int*);參數1是指向一個命令列字串,通常利用GetCommandLineW擷取。參數2是擷取命令列實參的個數。返回的字串數組所使用的記憶體,用LocalFree來釋放!
以下是MSDN的範例程式碼:是在CUI程式下CommandLinetoArgvW函數的使用
#include<windows.h>#include<tchar.h>#include<iostream>using namespace std;int _tmain(int argc, _TCHAR* argv[]){ LPWSTR *szArglist;//用於 int nArgs; int i; /* CommandLineToArgvW函數只有Unicode版本的,所以參數1也必須使用Unicode版本的GetCommandLineW來擷取完整的命令列 參數2是儲存完整命令列中一共有多少個命令列參數,包括檔案名稱參數。 CommandLineToArgvW函數返回的是一個Unicode字串指標數組的地址。 這個函數將參數1完整命令列分解成單獨的標記。 */ LPTSTR cmdLine; cmdLine = GetCommandLine(); printf("%ws\n", cmdLine);//這個是輸出完整命令列 szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs); if (NULL == szArglist) { wprintf(L"CommandLineToArgvW failed\n"); return 0; } else for (i = 0; i<nArgs; i++) printf("%d: %ws\n", i, szArglist[i]);//這個是輸出分解後放到字串數組中的內容 LocalFree(szArglist); system("pause"); return 0;}
在CUI程式下,進入點函數的argv函數就已經分解好了命令列參數,其實這個函數更大的用處是在GUI程式中,例如下面代碼的使用:
#include<windows.h>#include<tchar.h>int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR pszCmdLine, int nCmdShow){ LPWSTR *szArglist;//用於 int nArgs; LPTSTR cmdLine; cmdLine = GetCommandLineW(); szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs); LocalFree(szArglist); return 0;}
測試結果如下:
進程的環境變數
在我們熟知的Windows系統裡,一直有環境變數這一說,我們都還知道環境變數可在Windows介面裡的進階系統設定裡的環境變數中擷取或設定。但其實,環境變數真正是儲存在註冊表裡的,每個Windows系統的登錄編輯程式都在C:\Windows\regedit.exe,我們知道在可視化介面下(進階系統設定中開啟)有兩種環境變數,分別是系統變數和使用者變數。而系統變數和使用者變數分別在登錄編輯程式下的兩個路徑(系統變數路徑:HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\Environment;使用者變數路徑:HKEY_CURRENT_USER\Environment)。下面放個登錄編輯程式的:
好了,言歸正傳。其實每個進程被建立後都會有一個與它關聯的環境塊,也就是在進程地址空間內分配的一塊記憶體,記憶體塊包含的字串大概長這樣:
=::=::\ ...VarName1=VarValue1\0VarName2=VarValue2\0VarName3=VarValue3\0VarNameX=VarValueX\0\0
我們要注意的是等號左邊的VarName1、VarName2等都是環境變數的名稱,而等號右邊的VarValue1、VarValue2等都是環境變數的值。還有一個更重要的一點就是每行環境變數的賦值最後都有個‘\0’,這是字串結束符,後邊GetEnvironmentStrings函數遍曆完整的環境變數字串時有用。
我們有兩種方式來擷取完整的環境塊,第一種方式是調用GetEnvironmentStrings函數擷取完整的環境變數(還有GetEnvironmentVariable函數擷取的是單個指定環境變數名的值,下面會有使用案例)得到的完整環境塊的格式和前面描述的一樣;第二種方式是CUI程式專用的,就是通過入口函數所接收的TCHAR *envp[]參數來實現。不同於GetEnvironmentStrings返回的值,GetEnvironmentStrings返回的是完整的環境塊,而envp是一個字串指標數組,每個指標都指向一個不同的環境變數(其定義採用常規的“名稱=值”的格式),在數組最後一個元素是一個NULL指標,代表這是數組的末尾,那麼我們就可以通過這個NULL指標作為遍曆的終止處,我們需要注意的是以等號開頭的那些無效字串在我們接收到envp之前就已經被移除了,所以不必進行處理只要擷取數組元素即可。
(1)下面先講GetEnvironmentStrings函數的使用案例:
這裡先放上等會要用到的兩個函數的函數簽名。
1.GetEnvironmentStrings函數用於擷取所有環境變數字串:
LPTCH WINAPI GetEnvironmentStrings(void);傳回值:成功時,返回指向儲存環境變數的緩衝區;失敗時,傳回值為NULL。
2.FreeEnvironmentStrings函數用來釋放由GetEnvironmentStrings返回的記憶體塊:
BOOL WINAPI FreeEnvironmentStrings( __in LPTCH lpszEnvironmentBlock);傳回值:成功時,返回非零值;失敗時,返回零值,可調用GetLastError()查看進一步錯誤訊息。
#include <windows.h>#include <tchar.h>#include <stdio.h>#include<strsafe.h>int _tmain(){ LPTSTR lpszVariable; LPTCH lpvEnv;//LPTCH就是WCHAR *資料類型,指向寬字元的指標變數 size_t iTarget; //調用GetEnvironmentStrings函數擷取完整的環境變數記憶體塊,並讓lpvEnv指向這個記憶體塊 lpvEnv = GetEnvironmentStrings(); //如果擷取的環境塊為空白,則該函數調用失敗,並擷取錯誤碼 if (lpvEnv == NULL) { _tprintf(TEXT("GetEnvironmentStrings failed(%d)\n"), GetLastError()); return 0; } //lpvEnv指向的環境變數字串是以NULL分隔的,即‘\0‘分隔,可以回去看前面我展示的環境字串的大概格式。而字串最後是以NULL結尾的 lpszVariable = (LPTSTR)lpvEnv; while (*lpszVariable) { _tprintf(TEXT("%s\n"), lpszVariable); StringCchLength(lpszVariable, 1000, &iTarget);//PATH的值太長,我設1000為最大允許字元數 lpszVariable += iTarget + 1;//移動指標,訪問下一環境變數的值 } //如果GetEnvironmentStrings函數返回的記憶體塊不用了,記得要釋放掉 FreeEnvironmentStrings(lpvEnv); system("pause"); return 1;}
運行結果如下:
(2)下面是GetEnvironmentVariable函數的使用案例:
這裡先放上GetEnvironmentVariable函數簽名。
1.GetEnvironmentVariable函數用於擷取指定的環境變數:
DWORD WINAPI GetEnvironmentVariable( __in_opt LPCTSTR lpName, //環境變數名 __out_opt LPTSTR lpBuffer, //指向儲存環境變數值的緩衝區 __in DWORD nSize //緩衝區大小(字元數));傳回值:成功時,返回真實的環境變數值大小,不包括null結束符;如果lpBuffer大小不足,則傳回值是實際所需的字元數大小,lpBuffer內容就未被賦值;失敗時,返回0;如果指定的環境變數找不到,GetLastError()返回ERROR_ENVVAR_NOT_FOUND。
#include <windows.h>#include <tchar.h>#include <stdio.h>#include<strsafe.h>int _tmain(){ TCHAR szBuffer[1000]; DWORD dwResult = GetEnvironmentVariable(TEXT("PATH"), szBuffer, 1000); if (dwResult != 0) { _tprintf(TEXT("PATH=%s"), szBuffer); } else { _tprintf(TEXT("function call falid!")); } system("pause"); return 1;}
運行結果如下:
(2)下面是SetEnvironmentVariable函數的使用案例:
這裡先放上SetEnvironmentVariable函數簽名,後面使用案例有幾個注意點需要重視。
1.SetEnvironmentVariable函數用於設定指定的環境變數:
BOOL WINAPI SetEnvironmentVariable( __in LPCTSTR lpName, //環境變數名,當該值不存在且lpValue不為NULL時,將建立一個新的環境變數 __in_opt LPCTSTR lpValue //環境變數值);傳回值:成功時,返回非零值;失敗時,返回零值,調用GetLastError()查看具體的錯誤資訊。該函數對系統內容變數以及其他進程的環境變數不起作用!
在寫測試程式前,我先在我的電腦->屬性->進階系統設定->環境變數->使用者變數處添加一個自訂的環境變數MyPath,環境變數值為woaini。呃。。。值不是重點,大概長下面那樣。
好了,準備工作做好了,現在重點要關閉VS,重新開VS再運行測試代碼(先思考為什麼,如果不重開VS會有什麼現象,後面講注意點有解釋重開VS的原因),現在放測試代碼:
#include <windows.h>#include <tchar.h>#include <stdio.h>int _tmain(int argc,TCHAR *argv[],TCHAR *envp[]){ TCHAR szBuffer[1000];//用於儲存擷取的環境變數的值 DWORD dwResult1 = GetEnvironmentVariable(TEXT("MyPath"), szBuffer, 1000);//先擷取我們前面已經設定好的MyPath環境變數的值,如果沒錯應該是woaini,但如果你測試時擷取不到,該函數返回0,那麼就要看看後面我講的注意點了哦。 if (dwResult1 != 0) { _tprintf(TEXT("MyPath=%s\n"), szBuffer); } else { _tprintf(TEXT("function call falid!\n")); } SetEnvironmentVariable(TEXT("MyPath"), TEXT("I love you"));//這裡為我建立的MyPath環境變數重新修改值為I love you,注意,其實這隻是修改當前進程的環境塊,而未影響系統或使用者的環境塊 DWORD dwResult2 = GetEnvironmentVariable(TEXT("MyPath"), szBuffer, 1000);//這裡重新擷取以下修改後的MyPath環境變數的值 if (dwResult2 != 0) { _tprintf(TEXT("MyPath=%s\n"), szBuffer); } else { _tprintf(TEXT("function call falid!\n")); } system("pause"); return 0;}
如果執行步驟沒錯,那麼運行結果是下面這樣的:
好了,注意點有以下幾點:
1.為什麼要重開VS,GetEnvironmentVariable函數才能正確擷取前面我們建立的環境變數MyPath?這是因為我們之前講過每個進程在建立時就被分配了一個環境塊,而這個環境塊就是Windows系統賦予的,那麼我們可以猜測,當運行VS,就已經在內部存好了我們將要分配的環境塊內容,而我們是VS運行後再建立環境變數MyPath,那麼VS儲存的這塊內容還沒更新呢,所以函數當然擷取不到,我們只能重開VS了。這也只是我的猜測,是為了更好理解GetEnvironmentVariable函數,如有其他看法的,可以留言探究哦。
2.GetEnvironmentVariable函數對系統內容變數以及其他進程的環境變數不起作用,因為建立了一個進程,就已經為進程分配好環境塊了,我們通過GetEnvironmentVariable函數添加、修改或刪除環境塊內容,也只是添加、修改或刪除進程的環境塊,而非Windows系統或使用者的環境塊。
(3)下面是CUI程式入口函數TCHAR *envp[]參數的使用案例:
這裡就不自己寫代碼了,直接放上書本P76頁的範例程式碼(修改過)。
#include <windows.h>#include <tchar.h>#include <stdio.h>int _tmain(int argc,TCHAR *argv[],TCHAR *envp[]){ int current = 0;//用於環境變數計數 PTSTR *pElement = (PTSTR *)envp;//建立新的指標指向CUI程式的envp數組 PTSTR pCurrent = NULL;//用於遍曆envp數組元素的指標 while (pElement != NULL) { //取數組的元素 pCurrent = (PTSTR)(*pElement); //前面說過數組末尾是NULL指標,所以當遍曆到NULL則將pElement置NULL,接著就跳出迴圈了 if (pCurrent == NULL) { pElement = NULL; } else { //列印遍曆到的環境變數 _tprintf(TEXT("[%u] %s\r\n"), current, pCurrent); current++;//計數+1 pElement++;//指向下一個數組元素 } } system("pause"); return 0;}
運行結果如下:
(4)下面是ExpandEnvironmentStrings函數的使用案例:
通過前面註冊表的瞭解,我們可以細心發現,有些環境變數的值含有兩個百分比符號(%)之間的字串,這種字串叫做可替換字串,顧名思義,我們可以通過函數ExpandEnvironmentStrings函數替換掉可替換字串。也可以發現,這種可替換字串只有在註冊表才能看到,而在我的電腦->屬性->進階系統設定->環境變數或通過其他方式擷取整個完整的環境變數都看不到可替換字串這種形式。下面,我先放上ExpandEnvirnmentStrings函數的函數簽名:
DWORD WINAPI ExpandEnvironmentStrings( _In_ LPCTSTR lpSrc, _Out_opt_ LPTSTR lpDst, _In_ DWORD nSize);參數1:一個包含可替換字串的字串地址(也叫擴充字元串),例如:TEXT("PATH=%PATH%")參數2:用於接收擴充字元串的一個緩衝區的地址參數3:這個緩衝區的最大大小,用字元數來表示。傳回值:儲存擴充字元串所需的緩衝區的大小,用字元數表示,若參數3小於這個傳回值,%%變數就不會擴充,而是被替換為空白字串,所以一般要調用兩次ExpandEnvironmentStrings函數。
#include <windows.h>#include <tchar.h>#include <stdio.h>int _tmain(int argc,TCHAR *argv[],TCHAR *envp[]){ //第一次調用ExpandEnvironmentStrings是為了擷取儲存擴充字元串所需的緩衝區大小,所以函數參數2可以為NULL,參數3為0 DWORD chValue = ExpandEnvironmentStrings(TEXT("USERPROFILE=‘%USERPROFILE%‘"), NULL, 0); PTSTR pszBuffer = new TCHAR[chValue];//動態建立chValue大小的緩衝區,最後記得釋放掉動態建立的空間 chValue = ExpandEnvironmentStrings(TEXT("USERPROFILE=‘%USERPROFILE%‘"), pszBuffer, chValue);//這次調用才是真正擷取替換後的字串 _tprintf(TEXT("%s\r\n%d"), pszBuffer,chValue);//列印擴充字元串的緩衝區和字元數目 delete[]pszBuffer;//釋放動態建立的空間 system("pause"); return 0;}
運行結果如下:
進程的當前磁碟機和目錄
有一些Windows函數的調用需要提供路徑,例如:CreateFile函數開啟一個檔案(未指定完整路徑名,只有一個檔案名稱),那麼該函數就會在當前磁碟機(例如:C、D、E磁碟)的目前的目錄尋找檔案和目錄。系統在內部追蹤記錄著一個進程的當前磁碟機和目錄,我們可以擷取進程的當前磁碟機和目錄,也可以修改進程的當前磁碟機和目錄。
下面給出分別擷取和設定當前磁碟機和目錄的函數簽名:
1.GetCurrentDirectory函數擷取進程目前的目錄:
DWORD WINAPI GetCurrentDirectory( _In_ DWORD nBufferLength, _Out_ LPTSTR lpBuffer);nBufferLength:lpBuffer指標指向記憶體塊的大小(單位TCHAR);lpBuffer:接收當前路徑的記憶體塊。
2.SetCurrentDirectory函數設定進程目前的目錄
BOOL WINAPI SetCurrentDirectory( _In_ LPCTSTR lpPathName);lpPathName:需要被設定的目錄路徑
3.GetFullPathName函數擷取指定檔案的當前路徑:
DWORD WINAPI GetFullPathName( __in LPCTSTR lpFileName, __in DWORD nBufferLength, __out LPTSTR lpBuffer, __out LPTSTR *lpFilePart);lpFileName:檔案名稱nBufferLength:擷取全路徑的記憶體大小(TCHAR)lpBuffer:記憶體指標lpFilePart:檔案名稱最後一個元素,在lpBuffer中的位置。注意:這個函數,只是將當前路徑,粘貼到你給的檔案上,其他什麼也沒有做。
下面,我給出使用案例來領會這些函數的使用:
#include <windows.h>#include <tchar.h>#include <stdio.h>int _tmain(int argc,TCHAR *argv[],TCHAR *envp[]){ TCHAR szPath[MAX_PATH]; GetCurrentDirectory(MAX_PATH, szPath);//擷取進程當前路徑 _tprintf(L"%s\n", szPath); TCHAR *str = L"D:\\360Downloads\\";//設定的當前路徑 SetCurrentDirectory(str); //設定檔案的當前路徑,如果指定的str參數在電腦中存在這個路徑,那麼就設定成功,否則設定無效,還是採用前一個有效當前進程目錄 GetCurrentDirectory(MAX_PATH, szPath); _tprintf(L"%s\n", szPath); TCHAR *str1 = L"D:\\ddsdf\\";//設定的當前路徑 SetCurrentDirectory(str1); //設定檔案的當前路徑,如果指定的str參數在電腦中存在這個路徑,那麼就設定成功,否則設定無效,還是採用前一個有效當前進程目錄 GetCurrentDirectory(MAX_PATH, szPath); _tprintf(L"%s\n", szPath);//因為"D:\\ddsdf\\"路徑在我電腦裡不存在,所以SetCurrentDirectory函數設定失敗了 GetFullPathName(L"wxf1", MAX_PATH, szPath, NULL); //這個函數只是將進程當前路徑(szPath)粘貼到你給的檔案名稱(wxf1)上,其他什麼也沒有做,不做檢查 _tprintf(L"%s\n", szPath); system("pause"); return 0;}
運行結果如下:
通過上面的測試,我們可以得出以下幾點:
1.GetCurrentDirectory函數是擷取進程的目前的目錄,而函數參數1一般用MAX_PATH(宏定義為260)就很安全了,因為這是目錄名稱或檔案名稱得最大字元數了。
2.SetCurrentDirectory函數設定進程的目前的目錄,而如果該函數參數指定的路徑在本電腦中不存在,那麼就設定無效,還是採用前一個有效當前進程目錄。
3.GetFullPathName函數只是將進程當前路徑(szPath)粘貼到你給的檔案名稱(wxf1)上,其他什麼也沒有做,不做檢查 。
4.為了更好理解GetCurrentDirectory函數擷取進程的目前的目錄這一功能,你可以將上面代碼產生的可執行檔放到案頭,再運行,那麼進程的目前的目錄就改變啦
判斷系統版本
......明天補充
Windows核心編程之核心總結(第四章 進程(二))(2018.6.17)