Python使用PEfile模塊實(shí)現(xiàn)分析PE文件
PeFile模塊是Python
中一個強(qiáng)大的便攜式第三方PE
格式分析工具,用于解析和處理Windows
可執(zhí)行文件。該模塊提供了一系列的API接口,使得用戶可以通過Python
腳本來讀取和分析PE文件的結(jié)構(gòu),包括文件頭、節(jié)表、導(dǎo)入表、導(dǎo)出表、資源表、重定位表等等。此外,PEfile模塊還可以幫助用戶進(jìn)行一些惡意代碼分析,比如提取樣本中的字符串、獲取函數(shù)列表、重構(gòu)導(dǎo)入表、反混淆等等。PEfile模塊是Python中處理PE文件的重要工具之一,廣泛應(yīng)用于二進(jìn)制分析、安全研究和軟件逆向工程等領(lǐng)域。
由于該模塊為第三方模塊,在使用之前讀者需要在命令行下執(zhí)行pip install pefile
命令安裝第三方庫,當(dāng)安裝成功后即可正常使用,如下所示則是該模塊的基本使用方法,讀者可自行學(xué)習(xí)理解。
1.打開并加載PE文件
如下這段代碼封裝并實(shí)現(xiàn)了OpenPeFile
函數(shù),可用于打開一個PE文件,在其內(nèi)部首先判斷了可執(zhí)行文件是否被壓縮如果被壓縮則會通過zipfile
模塊將壓縮包讀入內(nèi)存并調(diào)用C2BIP3
函數(shù)將數(shù)據(jù)集轉(zhuǎn)換為2字節(jié),接著再執(zhí)行pefile.PE()
函數(shù),該函數(shù)可用于將可執(zhí)行文件載入,至此讀者可在主函數(shù)內(nèi)通過pe.dump_dict()
的方式輸出該P(yáng)E文件的所有參數(shù),由于輸出的是字典,讀者可以使用字典與列表的方式靈活的提取出該程序的所有參數(shù)信息。
import sys import zipfile import pefile # 如果是Python3則轉(zhuǎn)換為2字節(jié) def C2BIP3(string): if sys.version_info[0] > 2: return bytes([ord(x) for x in string]) else: return string # 打開文件 def OpenPeFile(filename): # 判斷是否是ZIP壓縮包 if filename.lower().endswith('.zip'): try: oZipfile = zipfile.ZipFile(filename, 'r') file = oZipfile.open(oZipfile.infolist()[0], 'r', C2BIP3('infected')) except Exception: print(sys.exc_info()[1]) sys.exit() oPE = pefile.PE(data=file.read()) file.close() oZipfile.close() # 如果是空則 elif filename == '': oPE = False return oPE # 否則直接打開文件 else: oPE = pefile.PE(filename) return oPE if __name__ == "__main__": pe = OpenPeFile("d://lyshark.exe") print(pe.FILE_HEADER.dump()) print(pe.dump_dict())
2.解析PE頭部數(shù)據(jù)
如下代碼實(shí)現(xiàn)了解析PE結(jié)構(gòu)中頭部基本數(shù)據(jù),在GetHeader
函數(shù)內(nèi),我們首先通過pe.FILE_HEADER.Machine
成員判斷當(dāng)前讀入的文件的位數(shù)信息,通過pe.FILE_HEADER.Characteristics
可判斷PE文件的類型,通常為EXE可執(zhí)行文件或DLL動態(tài)鏈接庫文件,通過AddressOfEntryPoint
加上ImageBase
則可獲取到程序的實(shí)際裝載地址,壓縮數(shù)據(jù)的計(jì)算可通過hashlib
模塊對PE文件字節(jié)數(shù)據(jù)進(jìn)行計(jì)算摘要獲取,最后是附加數(shù)據(jù),通過get_overlay_data_start_offset
則可獲取到,并依次循環(huán)即可輸出所有附加數(shù)據(jù)。
import hashlib import pefile # 計(jì)算得到數(shù)據(jù)長度,自動使用推薦大小 def NumberOfBytesHumanRepresentation(value): if value <= 1024: return '%s bytes' % value elif value < 1024 * 1024: return '%.1f KB' % (float(value) / 1024.0) elif value < 1024 * 1024 * 1024: return '%.1f MB' % (float(value) / 1024.0 / 1024.0) else: return '%.1f GB' % (float(value) / 1024.0 / 1024.0 / 1024.0) # 獲取PE頭部基本信息 def GetHeader(pe): raw = pe.write() # 掃描基本信息 print("-" * 50) print("程序基本信息") print("-" * 50) if (hex(pe.FILE_HEADER.Machine) == "0x14c"): print("程序位數(shù): {}".format("x86")) if (hex(pe.FILE_HEADER.Machine) == "0x8664"): print("程序位數(shù): {}".format("x64")) if (hex(pe.FILE_HEADER.Characteristics) == "0x102"): print("程序類型: Executable") elif (hex(pe.FILE_HEADER.Characteristics) == "0x2102"): print("程序類型: Dynamic link library") if pe.OPTIONAL_HEADER.AddressOfEntryPoint: oep = pe.OPTIONAL_HEADER.AddressOfEntryPoint + pe.OPTIONAL_HEADER.ImageBase print("實(shí)際入口: {}".format(hex(oep))) print("映像基址: {}".format(hex(pe.OPTIONAL_HEADER.ImageBase))) print("虛擬入口: {}".format(hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint))) print("映像大小: {}".format(hex(pe.OPTIONAL_HEADER.SizeOfImage))) print("區(qū)段對齊: {}".format(hex(pe.OPTIONAL_HEADER.SectionAlignment))) print("文件對齊: {}".format(hex(pe.OPTIONAL_HEADER.FileAlignment))) print("區(qū)塊數(shù)量: {}".format(int(pe.FILE_HEADER.NumberOfSections + 1))) print('熵值比例: %f (Min=0.0, Max=8.0)' % pe.sections[0].entropy_H(raw)) # 計(jì)算壓縮數(shù)據(jù) print("-" * 50) print("計(jì)算壓縮數(shù)據(jù)") print("-" * 50) print('MD5 : %s' % hashlib.md5(raw).hexdigest()) print('SHA-1 : %s' % hashlib.sha1(raw).hexdigest()) print('SHA-256 : %s' % hashlib.sha256(raw).hexdigest()) print('SHA-512 : %s' % hashlib.sha512(raw).hexdigest()) # 掃描文件末尾是否存在附加數(shù)據(jù) print("-" * 50) print("掃描附加數(shù)據(jù)") print("-" * 50) overlayOffset = pe.get_overlay_data_start_offset() if overlayOffset != None: print("起始文件位置: 0x%08x"%overlayOffset) overlaySize = len(raw[overlayOffset:]) print("長度: 0x%08x %s %.2f%%"%(overlaySize, NumberOfBytesHumanRepresentation(overlaySize), float(overlaySize) / float(len(raw)) * 100.0)) print("MD5: %s" %hashlib.md5(raw[overlayOffset:]).hexdigest()) print("SHA-256: %s" %hashlib.sha256(raw[overlayOffset:]).hexdigest()) if __name__ == "__main__": pe = pefile.PE("d://lyshark.exe") GetHeader(pe)
3.解析節(jié)表數(shù)據(jù)
運(yùn)用PEFile
模塊解析節(jié)表也很容易,如下代碼中分別實(shí)現(xiàn)了兩個功能函數(shù),函數(shù)ScanSection()
用于輸出當(dāng)前文件的所有節(jié)表數(shù)據(jù),其中通過pe.FILE_HEADER.NumberOfSections
得到節(jié)表數(shù)量,并通過循環(huán)的方式依次解析pe.sections
中的每一個節(jié)中元素,函數(shù)CheckSection()
則可用于計(jì)算PE
文件節(jié)大小以及節(jié)MD5
值,完整代碼如下所示;
import hashlib import pefile # 計(jì)算得到數(shù)據(jù)長度,自動使用推薦大小 def NumberOfBytesHumanRepresentation(value): if value <= 1024: return '%s bytes' % value elif value < 1024 * 1024: return '%.1f KB' % (float(value) / 1024.0) elif value < 1024 * 1024 * 1024: return '%.1f MB' % (float(value) / 1024.0 / 1024.0) else: return '%.1f GB' % (float(value) / 1024.0 / 1024.0 / 1024.0) # 輸出所有的節(jié) def ScanSection(pe): print("-" * 100) print("{:10s}{:10s}{:10s}{:10s}{:10s}{:10s}{:10s}{:10s}". format("序號","節(jié)區(qū)名稱","虛擬偏移","虛擬大小","實(shí)際偏移","實(shí)際大小","節(jié)區(qū)屬性","熵值")) print("-" * 100) section_count = int(pe.FILE_HEADER.NumberOfSections + 1) for count,item in zip(range(1,section_count),pe.sections): print("%d\t\t\t%-10s\t0x%.8X\t0x%.8X\t0x%.8X\t0x%.8X\t0x%.8X\t%f" %(count,(item.Name).decode("utf-8"),item.VirtualAddress,item.Misc_VirtualSize,item.PointerToRawData,item.SizeOfRawData,item.Characteristics,item.get_entropy())) print("-" * 100) # 計(jì)算所有節(jié)的MD5 def CheckSection(pe): print("-" * 100) print("序號\t\t節(jié)名稱\t\t文件偏移\t\t大小\t\tMD5\t\t\t\t\t\t\t\t\t\t節(jié)大小") print("-" * 100) # 讀取PE文件到內(nèi)存 image_data = pe.get_memory_mapped_image() section_count = int(pe.FILE_HEADER.NumberOfSections + 1) for count,item in zip(range(1,section_count),pe.sections): section_data = image_data[item.PointerToRawData: item.PointerToRawData + item.SizeOfRawData - 1] data_size = NumberOfBytesHumanRepresentation(len(section_data)) hash_value = hashlib.md5(section_data).hexdigest() print("{}\t{:10s}\t{:10X}\t{:10X}\t{:30s}\t{}".format(count,(item.Name).decode("utf-8"),item.PointerToRawData,item.SizeOfRawData,hash_value,data_size)) print("-" * 100) if __name__ == "__main__": pe = pefile.PE("d://lyshark.exe") ScanSection(pe) CheckSection(pe)
4.節(jié)區(qū)RVA與FOA互轉(zhuǎn)
此處計(jì)算節(jié)偏移地址,相信讀者能理解,在之前的文章中我們詳細(xì)的介紹了PE文件如何進(jìn)行RVA
與FOA
以及VA
之間的轉(zhuǎn)換的,如果是在平時的惡意代碼分析中需要快速實(shí)現(xiàn)轉(zhuǎn)換那么使用Python將是一個不錯的選擇,如下代碼中RVAToFOA
可將一個RVA
相對地址轉(zhuǎn)換為FOA
文件偏移,FOAToRVA
則可實(shí)現(xiàn)將一個FOA文件偏移轉(zhuǎn)換為RVA先對地址,當(dāng)然PeFile模塊內(nèi)也提供了get_rva_from_offset
實(shí)現(xiàn)從FOA轉(zhuǎn)RVA,get_offset_from_rva
則是從RVA到FOA,讀者可自行選擇不同的轉(zhuǎn)換方式。
import pefile # 將RVA轉(zhuǎn)換為FOA的函數(shù) def RVAToFOA(pe,rva): for item in pe.sections: Section_Start = item.VirtualAddress Section_Ends = item.VirtualAddress + item.SizeOfRawData if rva >= Section_Start and rva < Section_Ends: return rva - item.VirtualAddress + item.PointerToRawData return -1 # 將FOA文件偏移轉(zhuǎn)換為RVA相對地址 def FOAToRVA(pe,foa): ImageBase = pe.OPTIONAL_HEADER.ImageBase NumberOfSectionsCount = pe.FILE_HEADER.NumberOfSections for index in range(0,NumberOfSectionsCount): PointerRawStart = pe.sections[index].PointerToRawData PointerRawEnds = pe.sections[index].PointerToRawData + pe.sections[index].SizeOfRawData if foa >= PointerRawStart and foa <= PointerRawEnds: rva = pe.sections[index].VirtualAddress + (foa - pe.sections[index].PointerToRawData) return rva return -1 # 內(nèi)部功能實(shí)現(xiàn)FOA->RVA互轉(zhuǎn) def inside(pe): # 從FOA獲取RVA 傳入十進(jìn)制 rva = pe.get_rva_from_offset(3952) print("對應(yīng)內(nèi)存RVA: {}".format(hex(rva))) # 從RVA獲取FOA 傳入十進(jìn)制 foa = pe.get_offset_from_rva(rva) print("對應(yīng)文件FOA: {}".format(foa)) if __name__ == "__main__": pe = pefile.PE("d://lyshark.exe") ref = RVAToFOA(pe,4128) print("RVA轉(zhuǎn)FOA => 輸出十進(jìn)制: {}".format(ref)) ref = FOAToRVA(pe,1056) print("FOA轉(zhuǎn)RVA => 輸出十進(jìn)制: {}".format(ref))
5.解析數(shù)據(jù)為Hex格式
如下代碼片段實(shí)現(xiàn)了對PE文件的各種十六進(jìn)制操作功能,封裝cDump()
類,該類內(nèi)由多個類函數(shù)可以使用,其中HexDump()
可用于將讀入的PE文件以16進(jìn)制方式輸出,HexAsciiDump()
則可用于輸出十六進(jìn)制以及所對應(yīng)的ASCII格式,GetSectionHex()
用于找到PE文件的.text
節(jié),并將此節(jié)內(nèi)的數(shù)據(jù)讀入到內(nèi)存中,這段代碼可以很好的實(shí)現(xiàn)對PE文件的十六進(jìn)制輸出與解析,讀者可在實(shí)際開發(fā)中使用。
import pefile from io import StringIO import sys import re dumplinelength = 16 def CIC(expression): if callable(expression): return expression() else: return expression def IFF(expression, valueTrue, valueFalse): if expression: return CIC(valueTrue) else: return CIC(valueFalse) class cDump(): def __init__(self, data, prefix='', offset=0, dumplinelength=16): self.data = data self.prefix = prefix self.offset = offset self.dumplinelength = dumplinelength # 輸出指定位置的十六進(jìn)制格式 def HexDump(self): oDumpStream = self.cDumpStream(self.prefix) hexDump = '' for i, b in enumerate(self.data): if i % self.dumplinelength == 0 and hexDump != '': oDumpStream.Addline(hexDump) hexDump = '' hexDump += IFF(hexDump == '', '', ' ') + '%02X' % self.C2IIP2(b) oDumpStream.Addline(hexDump) return oDumpStream.Content() def CombineHexAscii(self, hexDump, asciiDump): if hexDump == '': return '' countSpaces = 3 * (self.dumplinelength - len(asciiDump)) if len(asciiDump) <= self.dumplinelength / 2: countSpaces += 1 return hexDump + ' ' + (' ' * countSpaces) + asciiDump # 輸出指定位置的十六進(jìn)制格式以及ASCII字符串 def HexAsciiDump(self): oDumpStream = self.cDumpStream(self.prefix) hexDump = '' asciiDump = '' for i, b in enumerate(self.data): b = self.C2IIP2(b) if i % self.dumplinelength == 0: if hexDump != '': oDumpStream.Addline(self.CombineHexAscii(hexDump, asciiDump)) hexDump = '%08X:' % (i + self.offset) asciiDump = '' if i % self.dumplinelength == self.dumplinelength / 2: hexDump += ' ' hexDump += ' %02X' % b asciiDump += IFF(b >= 32 and b <= 128, chr(b), '.') oDumpStream.Addline(self.CombineHexAscii(hexDump, asciiDump)) return oDumpStream.Content() class cDumpStream(): def __init__(self, prefix=''): self.oStringIO = StringIO() self.prefix = prefix def Addline(self, line): if line != '': self.oStringIO.write(self.prefix + line + '\n') def Content(self): return self.oStringIO.getvalue() @staticmethod def C2IIP2(data): if sys.version_info[0] > 2: return data else: return ord(data) # 只輸出十六進(jìn)制數(shù)據(jù) def HexDump(data): return cDump(data, dumplinelength=dumplinelength).HexDump() # 輸出十六進(jìn)制與ASCII字符串 def HexAsciiDump(data): return cDump(data, dumplinelength=dumplinelength).HexAsciiDump() # 找到指定節(jié)并讀取hex數(shù)據(jù) def GetSectionHex(pe): ImageBase = pe.OPTIONAL_HEADER.ImageBase for item in pe.sections: # 判斷是否是.text節(jié) if str(item.Name.decode('UTF-8').strip(b'\x00'.decode())) == ".text": # print("虛擬地址: 0x%.8X 虛擬大小: 0x%.8X" %(item.VirtualAddress,item.Misc_VirtualSize)) VirtualAddress = item.VirtualAddress VirtualSize = item.Misc_VirtualSize ActualOffset = item.PointerToRawData StartVA = hex(ImageBase + VirtualAddress) StopVA = hex(ImageBase + VirtualAddress + VirtualSize) print("[+] 代碼段起始地址: {} 結(jié)束: {} 實(shí)際偏移:{} 長度: {}".format(StartVA, StopVA, ActualOffset, VirtualSize)) # 獲取到.text節(jié)區(qū)間內(nèi)的數(shù)據(jù) hex_code = pe.write()[ActualOffset: VirtualSize] return hex_code else: print("程序中不存在.text節(jié)") return 0 return 0 REGEX_STANDARD = '[\x09\x20-\x7E]' def ExtractStringsASCII(data): regex = REGEX_STANDARD + '{%d,}' return re.findall(regex % 4, data) def ExtractStringsUNICODE(data): regex = '((' + REGEX_STANDARD + '\x00){%d,})' return [foundunicodestring.replace('\x00', '') for foundunicodestring, dummy in re.findall(regex % 4, data)] # 將傳入Hex字符串以每16字符分割在一個列表內(nèi) def ExtractStrings(data): return ExtractStringsASCII(data) + ExtractStringsUNICODE(data) if __name__ == "__main__": pe = pefile.PE("d://lyshark.exe") # 得到.text節(jié)內(nèi)數(shù)據(jù) ref = GetSectionHex(pe) # 轉(zhuǎn)為十六進(jìn)制格式 dump_hex = HexDump(ref) print(dump_hex) # 打包為每16字符一個列表 dump_list = ExtractStrings(dump_hex) print(dump_list)
6.解析數(shù)據(jù)目錄表
數(shù)據(jù)目錄表用于記錄可執(zhí)行文件的數(shù)據(jù)目錄項(xiàng)在文件中的位置和大小。數(shù)據(jù)目錄表共有16個條目,每個條目都對應(yīng)著一個數(shù)據(jù)目錄項(xiàng),每個數(shù)據(jù)目錄項(xiàng)都描述了可執(zhí)行文件中某一部分的位置和大小。
數(shù)據(jù)目錄表的解析可以使用pe.OPTIONAL_HEADER.NumberOfRvaAndSizes
首先獲取到數(shù)據(jù)目錄表的個數(shù),接著二通過循環(huán)個數(shù)依次解包OPTIONAL_HEADER.DATA_DIRECTORY
里面的每一個列表,在循環(huán)列表時依次解包輸出即可。
import pefile # 將RVA轉(zhuǎn)換為FOA的函數(shù) def RVAToFOA(pe,rva): for item in pe.sections: Section_Start = item.VirtualAddress Section_Ends = item.VirtualAddress + item.SizeOfRawData if rva >= Section_Start and rva < Section_Ends: return rva - item.VirtualAddress + item.PointerToRawData return -1 # 掃描數(shù)據(jù)目錄表 def ScanOptional(pe): optional_size = pe.OPTIONAL_HEADER.NumberOfRvaAndSizes print("數(shù)據(jù)目錄表個數(shù): {}".format(optional_size)) print("-" * 100) print("編號 \t\t\t 目錄RVA\t\t 目錄FOA\t\t\t 長度\t\t 描述信息") print("-" * 100) for index in range(0,optional_size): va = int(pe.OPTIONAL_HEADER.DATA_DIRECTORY[index].VirtualAddress) print("%03d \t\t 0x%08X\t\t 0x%08X\t\t %08d \t\t" %(index, pe.OPTIONAL_HEADER.DATA_DIRECTORY[index].VirtualAddress, RVAToFOA(pe,va), pe.OPTIONAL_HEADER.DATA_DIRECTORY[index].Size ),end="") if index == 0: print("Export symbols") if index == 1: print("Import symbols") if index == 2: print("Resources") if index == 3: print("Exception") if index == 4: print("Security") if index == 5: print("Base relocation") if index == 6: print("Debug") if index == 7: print("Copyright string") if index == 8: print("Globalptr") if index == 9: print("Thread local storage (TLS)") if index == 10: print("Load configuration") if index == 11: print("Bound Import") if index == 12: print("Import Address Table") if index == 13: print("Delay Import") if index == 14: print("COM descriptor") if index == 15: print("NoUse") if __name__ == "__main__": pe = pefile.PE("d://lyshark.exe") ScanOptional(pe)
7.解析導(dǎo)入導(dǎo)出表
導(dǎo)入表和導(dǎo)出表都是PE文件中的重要數(shù)據(jù)結(jié)構(gòu),分別記錄著一個模塊所導(dǎo)入和導(dǎo)出的函數(shù)和數(shù)據(jù),如下所示則是使用PeFile
模塊實(shí)現(xiàn)對導(dǎo)入表與導(dǎo)出表的解析工作,對于導(dǎo)入表ScanImport
的解析需要通過pe.DIRECTORY_ENTRY_IMPORT
獲取到完整的導(dǎo)入目錄,并通過循環(huán)的方式輸出x.imports
中的數(shù)據(jù)即可,而對于導(dǎo)出表ScanExport
則需要在pe.DIRECTORY_ENTRY_EXPORT.symbols
導(dǎo)出符號中解析獲取。
import pefile # 輸出所有導(dǎo)入表模塊 def ScanImport(pe): print("-" * 100) try: for x in pe.DIRECTORY_ENTRY_IMPORT: for y in x.imports: print("[*] 模塊名稱: %-20s 導(dǎo)入函數(shù): %-14s" %((x.dll).decode("utf-8"),(y.name).decode("utf-8"))) except Exception: pass print("-" * 100) # 輸出所有導(dǎo)出表模塊 def ScanExport(pe): print("-" * 100) try: for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols: print("[*] 導(dǎo)出序號: %-5s 模塊地址: %-20s 模塊名稱: %-15s" %(exp.ordinal,hex(pe.OPTIONAL_HEADER.ImageBase + exp.address),(exp.name).decode("utf-8"))) except: pass print("-" * 100) if __name__ == "__main__": pe = pefile.PE("d://lyshark.exe") ScanImport(pe) ScanExport(pe)
以上就是Python使用PEfile模塊實(shí)現(xiàn)分析PE文件的詳細(xì)內(nèi)容,更多關(guān)于Python PEfile的資料請關(guān)注腳本之家其它相關(guān)文章!
- python中shapefile庫讀取shapefile文件信息
- Python?shapefile轉(zhuǎn)GeoJson的2種方式實(shí)例
- Python實(shí)現(xiàn)csv文件(點(diǎn)表和線表)轉(zhuǎn)換為shapefile文件的方法
- python geopandas讀取、創(chuàng)建shapefile文件的方法
- Python 根據(jù)數(shù)據(jù)模板創(chuàng)建shapefile的實(shí)現(xiàn)
- Python中shapefile轉(zhuǎn)換geojson的示例
- Python使用pyshp庫讀取shapefile信息的方法
相關(guān)文章
Python連接Postgres/Mysql/Mongo數(shù)據(jù)庫基本操作大全
在后端應(yīng)用開發(fā)中,經(jīng)常會用到Postgres/Mysql/Mongo這三種數(shù)據(jù)庫的基本操作,今天小編就給大家詳細(xì)介紹Python連接Postgres/Mysql/Mongo數(shù)據(jù)庫基本操作,感興趣的朋友一起看看吧2021-06-06使用Python和xlwt向Excel文件中寫入中文的實(shí)例
下面小編就為大家分享一篇使用Python和xlwt向Excel文件中寫入中文的實(shí)例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-04-04Python?pywin32實(shí)現(xiàn)word與Excel的處理
這篇文章主要介紹了Python?pywin32實(shí)現(xiàn)word與Excel的處理,pywin32處理Word大多數(shù)用于格式轉(zhuǎn)換,因?yàn)橐话阕x寫操作都可以借助python-docx實(shí)現(xiàn),除非真的有特殊要求,但大部分企業(yè)對Wrod操作不會有太多復(fù)雜需求2022-08-08Python實(shí)現(xiàn)動態(tài)循環(huán)輸出文字功能
這篇文章主要介紹了Python實(shí)現(xiàn)動態(tài)循環(huán)輸出文字功能,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-05-05