NodeJS研究筆記:利用Buffer類的位元據讀取介面解析ELF檔案格式
javascript 作為前端開發語言,自古來對位元據的讀取解析方面的支援都很薄弱,一般來說,解析位元據時,往往是將資料轉換成字串,然後運用各種字串操作技巧來實現位元據的讀取。
由於NodeJS 作為後台伺服器開發平台,數理邏輯的設計需求超越javascript作為前端語言時介面UI的設計需求,因此,加強位元據的讀取功能顯得越發重要,幸運的是,NodeJS提供了Buffer類,該類提供的一系列介面使得對位元據的讀取和解析變得異常方便,本文以解析ELF檔案格式為例,向大家展示NodeJS強大的位元據讀取功能。
它是在Linux上編譯的一個簡單無比的Hello World C 語言程式,在Linux上,readelf 工具是解析elf檔案的最佳工具,本文要使用NodeJS開發readelf 的 -h 命令列功能,即讀取elf 檔案的頭部資料,Linux工具readelf -h 讀取上述連結的elf檔案後,顯示資訊如下:
接下來,我們看看如何通過NodeJS逐步實現該功能, 我們先看看ELF檔案的頭格式定義:
#define EI_NIDENT 16 typedef struct { unsigned char e_ident[EI_NIDENT]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; ElfN_Addr e_entry; ElfN_Off e_phoff; ElfN_Off e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; uint16_t e_shentsize; uint16_t e_shnum; uint16_t e_shstrndx; } ElfN_Ehdr;
elf 二進位檔案的頭部開始有16位元組,用於告訴作業系統如何解析該檔案,我們先用代碼把這16位元組資訊讀取出來:
var fs = require('fs');var fileBuf;function readFile(fileName) { fileBuf = fs.readFileSync(fileName);}if (process.argv.length >= 4) { if (process.argv[2] === '-h') { readFile(process.argv[3]); console.log(fileBuf.slice(0,17)); }}
我們把上面的代碼儲存為readIdent.js, 將連結給的hello elf檔案下載到與代碼相同的目錄,運行後結果如下:
vcq9vavOxLz+tcTE2sjdtsHIoaOsuMO6r8r9t7W72LXEysfSu7j2QnVmZmVywOCjrNTauMPA4NbQo6zT0NK7uPbX1r3au7qz5cf4yv3X6aOs16jDxdPDwLS05rSi0qq94s72tcS2/r341sbK/b7do6xmaWxlQnVmLnNsaWNlKDAsMTcp1/fTw8rHvavX1r3au7q05sf4yv3X6bXEzbcxNrj219a92sihs/bAtKOsyLu689PDY29uc29sZS5sb2e9q9XiMTbX1r3atcS2/r341sbK/crks/a1vb/Y1sbMqKGj1eLNtzE219a92tbQo6zHsDi49tfWvdq2vNPQzNi2qLXEuqzS5aOsvdPPwsC0ztLDx77N0qq21NfFMTbX1r3atcTS4tLlvfjQ0L3itsGhozxiciAvPg0KZV9pZGVudCDK/dfptcTNt8vEuPbX1r3ao6zSsr7NysdlbGbOxLz+tcTNt8vEuPbX1r3ayse5zLaoy8C1xKOsvdDEp8r119ajrLfWsfDKx6O6MHg3ZiwgMHg0NSwgMHg0YywgMHg0NiwguvPI/bj219a92rbU06a1xEFTQ0lJwuvX1rf7yscmcnNxdW87RSZyc3F1bzssJnJzcXVvO0wmcnNxdW87LCZyc3F1bztGJnJzcXVvOywgztLDx7DRyc/D5rXEtPrC68nUvNO4xLavo6yw0cSnyvXX1rTy06Gz9sC0v7S/tKO6PC9wPg0KPHByZSBjbGFzcz0="brush:java;">var fs = require('fs');var fileBuf;function readFile(fileName) { fileBuf = fs.readFileSync(fileName);}if (process.argv.length >= 4) { if (process.argv[2] === '-h') { readFile(process.argv[3]); var head = fileBuf.slice(0,17); console.log(head); console.log("magic num: ", head.toString('ascii', 0, 5)); }}
執行後結果如下:
fileBuf.slice(0, 17) 返回的也是一個Buffer類對象,這個Buffer類的位元組緩衝區數組儲存的是fileBuf位元組緩衝區的頭16位元組資料,Buffer提供了一個介面函數toString, 可以對緩衝區中的位元據依據給定的格式進行解讀,toString(‘ascii’, 0, 5) 表示把緩衝區的頭4個位元組當做ascii字元所組成的字串,由於第一個位元組0x7f在ascii碼中對應的字元是不可列印的,因此console.log只輸出了後三個位元組對應的ascii字元,他們就是ELF.
e_ident 中的第五個位元組表示可執行檔的二進位架構,稱為EI_CLASS,如果該位元組取值為0,那麼表示該二進位檔案是無效檔案,如果是1,表示該檔案可運行在32位的機器上,如果是2,表示可運行在64位的機器上。
第六個位元組表示的是資料編碼格式,稱為EI_DATA, 取值0,表示編碼格式未知,取值1表示little-endian,取值2表示big-endian,little-endian的意思是如果有4個單位元組, 0x1,0x2,0x3,0x4 如果把他們當做一個32位元同時解析時,解析的數值結果是 0x04030201, 如果是big-endian,那麼解析的結果是0x01020304.
第七個位元組稱為EI_VERSION, 用來表示當前ELF檔案所對應的格式版本,取值0表示無效版本,取值1表示最新版本,ELF檔案格式是進過長時間演化而來的,在發展過程中經曆了不同的版本,不同版本,它的二進位格式是不同的,作業系統需要只噹噹前可執行檔對應的版本,才知道如何解析載入該檔案。
第八位元組稱為EI_OSABI,用於表明可以執行該檔案的作業系統類型,對應的值有:
ELFOSABI_NONE Same as ELFOSABI_SYSV
0 UNIX System V ABI
1 HP-UX ABI
2 NetBSD ABI
3 Linux ABI
4 Solaris ABI
5 IRIX ABI
6 FreeBSD ABI
7 TRU64 UNIX ABI
8 ARM architecture ABI
9 Stand-alone (embedded) ABI
取值0表示可以被UNIX系統載入執行,取值3表示可以被Linux載入執行。
第九位元組稱為EI_ABIVERSION, 一般取值為0.
從第九位元組之後的位元組都用於填充,沒有實際意義,接下來我們給代碼添加e_ident數組的解讀功能:
var fs = require('fs');var fileBuf;var elfHeader = {};var EI_NIDENT = 16;var readOffset = 0;var eiOSABI = ['UNIX System V ABI', 'UNIX System V ABI', 'HP-UX ABI', 'NetBSD ABI', 'Linux ABI', 'Solaris ABI','IRIX ABI', 'FreeBSD ABI', 'TRU64 UNIX ABI', 'ARM architecture ABI', 'Stand-alone (embedded) ABI'];var fileVersion = ['invalid version', 'current version'];function digestEIdent(eIdent) { elfHeader['magic'] = eIdent.toString('ascii', 0, 4); switch (eIdent[4]) { case 0: elfHeader['class'] = 'illegal file'; break; case 1: elfHeader['class'] = 'ELF32'; break; case 2: elfHeader['class'] = 'ELF64'; break; } elfHeader['data'] = 'illegal code format'; if (eIdent[5] === 1) { elfHeader['data'] = 'little endian'; } else if (eIdent[5] == 2) { elfHeader['data'] = 'bigger endian'; } elfHeader['version'] = eIdent[6]; elfHeader['osabi'] = eiOSABI[eIdent[7]]; elfHeader['abi version'] = eIdent[8];}function readFile(fileName) { fileBuf = fs.readFileSync(fileName);}if (process.argv.length >= 4) { if (process.argv[2] === '-h') { readFile(process.argv[3]); var eIdent = fileBuf.slice(0,17); digestEIdent(eIdent); console.log(elfHeader); }}
digestEIdent函數依照上面解釋的位元組意義,將每個位元組讀取出來,並把位元組對應的意義用字串表示,然後把他們對應的資訊填寫到elfHeader對象中,最後把資訊再輸出到控制台:
大家可以看到,我們的解析跟readelf工具對e_ident數組解析的結果是一致的
接下來是e_type, 它是兩位元組的資料類型,用於表明當前檔案類型,取值如下:
ET_NONE An unknown type.
ET_REL A relocatable file.
ET_EXEC An executable file.
ET_DYN A shared object.
ET_CORE A core file.
如果取值是ET_EXEC(2), 表明檔案是可執行檔,如果取值是ET_DYN(3)表明檔案是動態連結程式庫,Buffer類提供了介面,專門用來讀取兩位元組資料,它是
readUInt32LE, 該介面的輸入參數是讀取資料的位移,如果我們要讀取e_type,由於e_type的位置位移是第17位元組,因此通過fileBuf.readUInt16LE(17)就可以把它的值讀出來,同理,對應的介面 readUInt32LE可以讀取4位元組的資料。
接下來的兩位元組資料成為e_machine, 用來表明該可執行檔所對應的CPU類型。
e_machine在我們給定的檔案中,取值為62,對應的含義為:
AMD x86-64 architecture
接下來的四位元組資料叫e_version,它只有兩個取值,0表示檔案無效,1表示有效。Buffer類提供的4位元組讀取介面是readUInt32LE.
接下來的資料叫e_entry, 用來表示執行檔案被系統載入如記憶體後所在的虛擬位址,當檔案被載入如記憶體,系統會將寄存器EIP指向該地址,開始程式的運行,要注意的是該段資料的長度取決於作業系統的位元,還記得e_ident數組中的第5個位元組EI_CLASS吧,如果該位元組取值1,表明系統是32位,那麼e_entry也是32位的,讀取它可以直接使用readUInt32LE介面,EI_CLASS取值為2,那麼系統是64位,那麼e_entry長度就得是64位,八位元組,由於Buffer類沒有提供直接讀取八位元組的介面,因此該功能需要我們自己實現,實現的辦法是分別讀出兩個4位元組,然後將第二個4位元組左移32位後跟第一個4位元組串連起來,從而組成一個8位元組數,代碼實現如下:
function readUInt64LE(buf, readOffset) { var lowerPart = buf.readUInt32LE(readOffset); readOffset += 4; var higherPart = buf.readUInt32LE(readOffset); readOffset += 4; return '0x' + ((higherPart << 32) | lowerPart).toString(16); }
接下來的資料叫程式頭表位移e_phoff(program header table offset), 作業系統通過讀取程式頭表擷取程式的相關資訊,以便決定如何載入可執行檔,它的長度和解讀方法同上。
接下來的資料叫代碼節頭表e_shoff,跟上面的程式頭表性質類似,也是系統用例負載檔案所需要讀取的資訊,長度和解讀方法跟上面一樣
接下來的4位元組e_flags,用來設定一些與cpu相關的標誌位,當前始終設定為0.
接下來的2位元組e_ehsize用來表示整個elf檔案頭的長度。
接下來2位元組e_phentsize表示程式頭的大小,程式頭是一種資料結構,內容如下:
typedef struct {
uint32_t p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
uint32_t p_filesz;
uint32_t p_memsz;
uint32_t p_flags;
uint32_t p_align;
} Elf32_Phdr;
接下來2位元組e_phnum表示程式頭表中,包含多少個程式頭資料結構。
接下來的2位元組e_shentsize表示程式節表的長度
接下來2位元組e_shnum表示程式節頭表中有多少個元素。
最後的2位元組e_shstrndx叫節頭表字元索引
大家可能對一些資料區段所表示的含義不是很清楚,這些含義是什麼已經是載入器和連接器的內容,不屬於本文考慮的範圍,大家只要懂得如何通過 NodeJS讀取位元據就可以了,最後我把整個讀取代碼實現在檔案elfreader.js中:
var fs = require('fs');var fileBuf;var elfHeader = {};var EI_NIDENT = 16;var readOffset = 0;var eiOSABI = ['UNIX System V ABI', 'UNIX System V ABI', 'HP-UX ABI', 'NetBSD ABI', 'Linux ABI', 'Solaris ABI','IRIX ABI', 'FreeBSD ABI', 'TRU64 UNIX ABI', 'ARM architecture ABI', 'Stand-alone (embedded) ABI'];var eType = ['unknown type', 'relocatable file', 'an executable file', 'a shared file', 'a core file'];var fileVersion = ['invalid version', 'current version'];function digestEIdent(eIdent) { elfHeader['magic'] = eIdent.toString('ascii', 0, 4); switch (eIdent[4]) { case 0: elfHeader['class'] = 'illegal file'; break; case 1: elfHeader['class'] = 'ELF32'; break; case 2: elfHeader['class'] = 'ELF64'; break; } elfHeader['data'] = 'illegal code format'; if (eIdent[5] === 1) { elfHeader['data'] = 'little endian'; } else if (eIdent[5] == 2) { elfHeader['data'] = 'bigger endian'; } elfHeader['version'] = eIdent[6]; elfHeader['osabi'] = eiOSABI[eIdent[7]]; elfHeader['abi version'] = eIdent[8];}function readUInt64LE(buf, readOffset) { var lowerPart = buf.readUInt32LE(readOffset); readOffset += 4; var higherPart = buf.readUInt32LE(readOffset); readOffset += 4; return '0x' + ((higherPart << 32) | lowerPart).toString(16); }function readElfHeader(buf) { var eIdent = buf.slice(readOffset, readOffset + EI_NIDENT); digestEIdent(eIdent); readOffset += EI_NIDENT; elfHeader['type'] = eType[buf.readUInt16LE(readOffset)]; readOffset += 2; if (buf.readUInt16LE(readOffset) === 0x003e) { elfHeader['machine'] = "AMD x86-64 architecture"; } else { elfHeader['machine'] = buf.readUInt16LE(readOffset); } readOffset += 2; elfHeader['file version'] = fileVersion[buf.readUInt32LE(readOffset)]; readOffset += 4; if (elfHeader['class'] === 'ELF64') { elfHeader['entry point address'] = readUInt64LE(buf, readOffset); readOffset += 8; } else { elfHeader['entry point address'] = buf.readUInt32LE(readOffset); readOffset += 4; } if (elfHeader['class'] === 'ELF64') { elfHeader['program header offset from file'] = readUInt64LE(buf, readOffset); readOffset += 8; } else { elfHeader['program header offset from file'] = buf.readUInt32LE(readOffset); readOffset += 4; } if (elfHeader['class'] === 'ELF64') { elfHeader['section header offset from file'] = readUInt64LE(buf, readOffset); readOffset += 8; } else { elfHeader['section header offset from file'] = buf.readUInt32LE(readOffset); readOffset += 4; } elfHeader['flags'] = buf.readUInt32LE(readOffset); readOffset += 4; elfHeader['size of this header'] = buf.readUInt16LE(readOffset); readOffset += 2; elfHeader['size of program headers'] = buf.readUInt16LE(readOffset); readOffset += 2; elfHeader['number of program header'] = buf.readUInt16LE(readOffset); readOffset += 2; elfHeader['size of section header'] = buf.readUInt16LE(readOffset); readOffset += 2; elfHeader['number of section header'] = buf.readUInt16LE(readOffset); readOffset += 2; elfHeader['section header string table index'] = buf.readUInt16LE(readOffset); readOffset += 2; console.log(elfHeader);}function readFile(fileName) { fileBuf = fs.readFileSync(fileName);}if (process.argv.length >= 4) { readFile(process.argv[3]); if (process.argv[2] === '-h') { readElfHeader(fileBuf); }}
程式執行後結果如下:
程式的運行結果跟開始使用的readelf 工具所顯示的結果是一致的