NodeJS Research Note: using the Buffer class binary data reading interface to parse the ELF File Format

Source: Internet
Author: User

NodeJS Research Note: using the Buffer class binary data reading interface to parse the ELF File Format

Javascript, as a front-end development language, has poor support for reading and parsing binary data since ancient times. Generally, when parsing binary data, it is often to convert the data into strings, then, various string operations are used to read binary data.

As NodeJS is a backend server development platform, the design requirements of mathematical logic go beyond the design requirements of the Interface UI when javascript is used as the front-end language. Therefore, it becomes more important to enhance the reading function of binary data, fortunately, NodeJS provides the Buffer class, which provides a series of interfaces to facilitate reading and parsing binary data. This article takes parsing the ELF file format as an example, shows you the powerful binary data reading function of NodeJS.
It is a simple Hello World C language program compiled on Linux. on Linux, readelf is the best tool for parsing elf files, this article uses NodeJS to develop the readelf-h command line function, that is, to read the header data of the elf File. After the Linux tool readelf-h reads the elf file of the above link, the following information is displayed:

Next, let's take a look at how to gradually implement this function through NodeJS. Let's first look at the Header Format definition of the ELF File:

 #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;

The header of the elf binary file starts with 16 bytes, which is used to tell the operating system how to parse the file. We first read the 16 bytes of information using the code:

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));    }}

We store the above Code as readIdent. js and download the linked hello elf file to the same directory as the code. The result is as follows:
Vcq9vavOxLz + release/r341sbK/release/r341sbK/crks/a1vb/release + release/release = "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)); }}

The result is as follows:

FileBuf. slice (0, 17) returns a Buffer class object. The Buffer Class byte Buffer array stores the first 16 bytes of data in the fileBuf byte Buffer. The Buffer provides an interface function toString, the binary data in the buffer zone can be interpreted according to the given format. toString ('ascii ', 0, 5) indicates that the first four bytes of the buffer zone are treated as strings consisting of ascii characters, because the characters corresponding to the first byte 0x7f In the ascii Code cannot be printed, the console. log outputs only the ascii characters corresponding to the last three bytes. They are ELF.

The fifth byte in e_ident indicates the binary architecture of the executable file, which is called EI_CLASS. If the value of this byte is 0, it indicates that the binary file is invalid. If it is 1, indicates that the file can run on a 32-bit machine. If it is 2, it can run on a 64-bit machine.

The sixth byte represents the data encoding format, which is referred to as EI_DATA. The value 0 indicates that the encoding format is unknown. The value 1 indicates little-endian, and the value 2 indicates big-endian, little-endian means that if there are 4 single-byte, 0x4, if they are resolved as a 32-bit simultaneously, the parsed value is 0x04030201. If it is big-endian, the parsed result is 0x01020304.

The seventh byte is referred to as EI_VERSION, which indicates the format version corresponding to the current ELF file. The value 0 indicates the invalid version, the value 1 indicates the latest version, and the ELF File Format evolved over a long period of time, during the development process, the binary format of different versions is different, and the operating system only needs the version corresponding to the current executable file, to learn how to parse and load the file.

The eighth byte is referred to as EI_OSABI, which indicates the operating system type of the file that can be executed. The corresponding values include:
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
The value 0 indicates that the file can be loaded and executed by the UNIX system. The value 3 indicates that the file can be loaded and executed by Linux.

The ninth byte is referred to as EI_ABIVERSION. Generally, the value is 0.
The Bytes after the ninth byte are used for filling, which has no practical significance. Next we will add the interpretation function of the e_ident array to the Code:

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);    }}

The digestEIdent function reads each byte according to the byte meaning explained above, and represents the meaning of the byte with a string, and then fills in the corresponding information in the elfHeader object, finally, output the information to the console:

As you can see, our Parsing is consistent with that of the readelf tool for parsing the e_ident array.

The next step is e_type, which is a two-byte data type used to indicate the current file type. The values are as follows:
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.
If the value is ET_EXEC (2), it indicates that the file is an executable file. If the value is ET_DYN (3), it indicates that the file is a dynamic link library, and the Buffer class provides interfaces, specifically used to read two bytes of data, it is
ReadUInt32LE, the input parameter of this interface is the data read displacement. If we want to read e_type, because the location offset of e_type is 17th bytes, we use fileBuf. the value of readUInt16LE (17) can be read. Similarly, the corresponding interface readUInt32LE can read 4 bytes of data.

The following two bytes of data become e_machine, which indicates the CPU type corresponding to the executable file.
In the given file, the value of e_machine is 62, which means:
AMD x86-64 architecture

The next four bytes of data are called e_version. It has only two values. 0 indicates that the file is invalid, and 1 indicates that the file is valid. The 4-byte read interface provided by the Buffer class is readUInt32LE.

The following data is called e_entry, which indicates the virtual address where the execution file is loaded by the system such as memory. When the file is loaded such as memory, the system points the register EIP to this address, when running the program, note that the length of the Data Segment depends on the number of digits in the operating system. Remember to set the length to 5th bytes in the e_ident array to EI_CLASS. If the value of this byte is 1, it indicates that the system is 32-bit, so the e_entry is also 32-bit. to read the system, you can directly use the readUInt32LE interface. If the value of EI_CLASS is 2, the system is 64-bit, the e_entry length must be 64-bit and eight-character segments. Since the Buffer class does not provide an interface for Directly Reading eight-character segments, we need to implement this function by ourselves, the implementation method is to read two 4 bytes respectively, and then shift the second 4 bytes to the left and connect it with the first 4 bytes to form an 8-byte number. The code implementation is as follows:

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); }

The following data is called the program header table offset e_phoff (program header table offset). The operating system reads program information from the program header table to determine how to load executable files, its length and interpretation method are the same as above.

The following data is called the code header table e_shoff, which is similar to the program header table. It is also the information to be read when a file is loaded in the system use case. The length and interpretation methods are the same as those above.

The next 4-byte e_flags is used to set some cpu-related flags. Currently, it is always set to 0.

The next two bytes, e_ehsize, are used to indicate the length of the entire elf file header.

The next 2 bytes e_phentsize indicates the size of the program header. The program header is a data structure with the following content:

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;

The next two bytes of e_phnum indicate the number of program header data structures in the program header table.

The next 2-byte e_shentsize indicates the length of the program section table.

The next two bytes, e_shnum, indicate the number of elements in the program header table.

The last 2-byte e_shstrndx is the character index of the node header table.

You may not be very clear about the meaning of some data segments. What these meanings are already the content of loaders and connectors, which is beyond the scope of this article, you only need to know how to read binary data through NodeJS. Finally, I implement the entire read code in the file 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);    }}

The execution result is as follows:

The running result of the program is consistent with that displayed by the readelf tool.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.