NodeJS研究笔记:利⽤Buffer类的⼆进制数据读取接⼝解析ELF⽂件格式
javascript 作为前端开发语⾔,⾃古来对⼆进制数据的读取解析⽅⾯的⽀持都很薄弱,⼀般来说,解析⼆进制数据时,往往是将数据转换成字符串,然后运⽤各种字符串操作技巧来实现⼆进制数据的读取。
由于NodeJS 作为后台服务器开发平台,数理逻辑的设计需求超越javascript作为前端语⾔时界⾯UI的设计需求,因此,加强⼆进制数据的读取功能显得越发重要,幸运的是,NodeJS提供了Buffer类,该类提供的⼀系列接⼝使得对⼆进制数据的读取和解析变得异常⽅便,本⽂以解析ELF⽂件格式为例,向⼤家展⽰NodeJS强⼤的⼆进制数据读取功能。
本⽂要读取的elf⽂件链接如下:
它是在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⽂件下载到与代码相同的⽬录,运⾏后结果如下:
⼤家可以发现,我们打印出来的数据跟前⾯readelf -h 后得到的magic部分是完全⼀样的。
我们解读下代码,⾸先我们⽤语句 fs = require(‘fs’); 将NodeJS的⽂件读取模块加载到程序,接下来调⽤fs模块的readFileSync以堵塞的⽅式将⽂件的内容读取,该函数返回的是⼀个Buffer类,在该类中,有⼀个字节缓冲区数组,专门⽤来存储要解析的⼆进制数
据,fileBuf.slice(0,17)作⽤是将字节缓存区数组的头16个字节取出来,然后⽤console.log将这16字节的⼆进制数输出到控制台。这头16字节中,前8个字节都有特定的含义,接下来我们就要对着16字节的意义进⾏解读。
e_ident 数组的头四个字节,也就是elf⽂件的头四个字节是固定死的,叫魔术字,分别是:0x7f, 0x45, 0x4c, 0x46, 后三个字节对应的ASCII码字符是’E’,’L’,’F’, 我们把上⾯的代码稍加改动,把魔术字打印出来看看:
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: ", String('ascii', 0, 5));
}
}
执⾏后结果如下:评选投票
fileBuf.slice(0, 17) 返回的也是⼀个Buffer类对象,这个Buffer类的字节缓冲区数组存储的是fileBuf字节缓冲区的头16字节数
据,Buffer提供了⼀个接⼝函数toString, 可以对缓冲区中的⼆进制数据依据给定的格式进⾏解读,toSt
ring(‘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 elfHeader = {};
var EI_NIDENT = 16;
var readOfft = 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'] = String('ascii', 0, 4);
switch (eIdent[4]) {
ca0:
elfHeader['class'] = 'illegal file';
break;
ca1:
elfHeader['class'] = 'ELF32';
break;
矮的用英语怎么说ca2:
elfHeader['class'] = 'ELF64';
break;
}
elfHeader['data'] = 'illegal code format';
if (eIdent[5] === 1) {
elfHeader['data'] = 'little endian';
}
el 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字节,因此通过驾照学习
接下来的两字节数据成为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, readOfft) {
var lowerPart = adUInt32LE(readOfft);
readOfft += 4;
var higherPart = adUInt32LE(readOfft);
readOfft += 4;
return'0x' + ((higherPart << 32) | lowerPart).toString(16);
}
接下来的数据叫程序头表偏移e_phoff(program header table offt), 操作系统通过读取程序头表获取程序的相关信息,以便决定如何加载可执⾏⽂件,它的长度和解读⽅法同上。
接下来的数据叫代码节头表e_shoff,跟上⾯的程序头表性质类似,也是系统⽤例加载⽂件所需要读取的信息,长度和解读⽅法跟上⾯⼀样
接下来的4字节e_flags,⽤来设置⼀些与cpu相关的标志位,当前始终设置为0.
脚心有痣
接下来的2字节e_ehsize⽤来表⽰整个elf⽂件头的长度。
惩罚打屁股
接下来2字节e_phentsize表⽰程序头的⼤⼩,程序头是⼀种数据结构,内容如下: