1680 字
5 分钟
JavaScript二进制处理
JavaScript 二进制数据处理详解
在逆向工程和底层开发中,二进制数据处理是核心技能。本文详细介绍 JavaScript 中的 ArrayBuffer、TypedArray、DataView 以及相关位操作符。
目录
- JavaScript 二进制数据处理详解
一、ArrayBuffer:二进制数据的容器
1.1 什么是 ArrayBuffer
ArrayBuffer 是一块原始的二进制数据缓冲区,它不可直接读写,必须通过视图(TypedArray 或 DataView)来操作。
// 创建一个 16 字节的缓冲区const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16console.log(buffer instanceof ArrayBuffer); // true关键特性:
- 固定长度,创建后无法改变大小
- 存储的是原始二进制数据(字节序列)
- 无法直接访问,需要视图
1.2 内存布局
ArrayBuffer (16 bytes)┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │10 │11 │12 │13 │14 │15 │└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ ↑ ↑字节索引 0 字节索引 15二、TypedArray:类型化数组视图
2.1 TypedArray 类型一览
| 类型 | 字节数 | 取值范围 | 描述 |
|---|---|---|---|
Int8Array | 1 | -128 ~ 127 | 8位有符号整数 |
Uint8Array | 1 | 0 ~ 255 | 8位无符号整数 |
Uint8ClampedArray | 1 | 0 ~ 255 | 8位无符号整数(溢出钳制) |
Int16Array | 2 | -32768 ~ 32767 | 16位有符号整数 |
Uint16Array | 2 | 0 ~ 65535 | 16位无符号整数 |
Int32Array | 4 | -2147483648 ~ 2147483647 | 32位有符号整数 |
Uint32Array | 4 | 0 ~ 4294967295 | 32位无符号整数 |
Float32Array | 4 | IEEE 754 单精度浮点数 | 32位浮点数 |
Float64Array | 8 | IEEE 754 双精度浮点数 | 64位浮点数 |
BigInt64Array | 8 | -2^63 ~ 2^63-1 | 64位有符号 BigInt |
BigUint64Array | 8 | 0 ~ 2^64-1 | 64位无符号 BigInt |
2.2 Uint8Array:最常用的视图
Uint8Array 是逆向中最常见的视图,每个元素代表一个字节(0-255)。
const buffer = new ArrayBuffer(4);const uint8 = new Uint8Array(buffer);
// 写入数据uint8[0] = 0x48; // 'H'uint8[1] = 0x65; // 'e'uint8[2] = 0x6C; // 'l'uint8[3] = 0x6F; // 'o'
console.log(uint8); // Uint8Array(4) [72, 101, 108, 111]
// 溢出行为:取低8位uint8[0] = 300; // 300 = 0x12C → 取低8位 0x2C = 44console.log(uint8[0]); // 44
uint8[0] = -1; // -1 → 0xFF = 255console.log(uint8[0]); // 2552.3 Uint8ClampedArray:特殊的溢出处理
与 Uint8Array 不同,Uint8ClampedArray 在溢出时会钳制到边界值:
const clamped = new Uint8ClampedArray(4);const normal = new Uint8Array(4);
clamped[0] = 300; // 超出上限 → 钳制到 255normal[0] = 300; // 取模 → 44
clamped[1] = -10; // 低于下限 → 钳制到 0normal[1] = -10; // 取模 → 246
console.log(clamped[0], normal[0]); // 255, 44console.log(clamped[1], normal[1]); // 0, 246应用场景:Canvas 像素操作(RGBA 值必须在 0-255 范围内)
2.4 Int8Array:有符号字节
const int8 = new Int8Array(4);
int8[0] = 127; // 最大值int8[1] = -128; // 最小值int8[2] = 255; // 255 作为有符号数 → -1
console.log(int8); // Int8Array(4) [127, -128, -1, 0]有符号数的表示(补码):
正数:直接表示 127 = 0111 1111
负数:补码表示 -1 = 1111 1111 (255 的无符号表示) -128 = 1000 0000 (128 的无符号表示)2.5 16位视图:Int16Array / Uint16Array
const buffer = new ArrayBuffer(4);const uint16 = new Uint16Array(buffer);const uint8 = new Uint8Array(buffer);
// 写入一个 16 位整数uint16[0] = 0x1234;
// 查看字节表示(小端序)console.log(uint8[0], uint8[1]); // 52, 18 → 0x34, 0x12字节序问题:
- 小端序(Little-Endian):低位字节在前(x86/ARM 默认)
- 大端序(Big-Endian):高位字节在前(网络协议)
数值 0x12345678 在内存中的存储:
小端序:78 56 34 12 (低地址存低位字节)大端序:12 34 56 78 (低地址存高位字节)2.6 32位视图:Int32Array / Uint32Array
const buffer = new ArrayBuffer(8);const uint32 = new Uint32Array(buffer);const uint8 = new Uint8Array(buffer);
uint32[0] = 0xDEADBEEF;
// 小端序存储console.log(uint8[0].toString(16)); // 'ef'console.log(uint8[1].toString(16)); // 'be'console.log(uint8[2].toString(16)); // 'ad'console.log(uint8[3].toString(16)); // 'de'2.7 浮点数视图:Float32Array / Float64Array
const buffer = new ArrayBuffer(8);const float32 = new Float32Array(buffer);const float64 = new Float64Array(buffer);const uint8 = new Uint8Array(buffer);
// 32位浮点数float32[0] = 3.14159;console.log(uint8.slice(0, 4)); // 查看字节表示
// 64位浮点数(JavaScript Number 的内部表示)float64[0] = 3.14159;console.log(uint8.slice(0, 8)); // 查看字节表示IEEE 754 浮点数结构:
Float32(32位单精度):┌─┬────────┬───────────────────────┐│S│ Exp(8) │ Mantissa(23) │└─┴────────┴───────────────────────┘
Float64(64位双精度):┌─┬──────────┬─────────────────────────────────┐│S│ Exp(11) │ Mantissa(52) │└─┴──────────┴─────────────────────────────────┘
S = 符号位 (0正1负)Exp = 指数(偏移表示)Mantissa = 尾数(隐含前导1)2.8 BigInt 视图:BigInt64Array / BigUint64Array
const buffer = new ArrayBuffer(16);const bigInt64 = new BigInt64Array(buffer);const bigUint64 = new BigUint64Array(buffer);
bigInt64[0] = 9223372036854775807n; // 最大正数 (2^63 - 1)bigInt64[1] = -9223372036854775808n; // 最小负数 (-2^63)
bigUint64[0] = 18446744073709551615n; // 最大值 (2^64 - 1)
console.log(bigInt64[0]); // 9223372036854775807nconsole.log(bigUint64[0]); // 18446744073709551615n2.9 同一 ArrayBuffer 的多视图
关键概念:多个视图可以共享同一个 ArrayBuffer,修改会相互影响。
const buffer = new ArrayBuffer(8);
const uint8 = new Uint8Array(buffer);const uint32 = new Uint32Array(buffer);const float64 = new Float64Array(buffer);
// 通过 uint32 写入uint32[0] = 0x41424344;
// 通过 uint8 读取(查看字节)console.log(String.fromCharCode(uint8[0], uint8[1], uint8[2], uint8[3]));// 小端序:'DCBA' 或 'ABCD'(取决于字节序)
// 通过 float64 读取(无意义的浮点数)console.log(float64[0]); // 一个很小的数应用场景:
- 解析自定义二进制协议
- 类型转换(如浮点数与整数的底层表示转换)
- 加密算法实现
三、DataView:灵活的字节操作
3.1 为什么需要 DataView
TypedArray 有两个限制:
- 平台字节序固定(通常是小端序)
- 类型固定
DataView 可以:
- 自由指定字节序
- 在任意位置读写任意类型
const buffer = new ArrayBuffer(16);const view = new DataView(buffer);
// 在偏移 0 处写入 32 位整数(大端序)view.setInt32(0, 0x12345678, false); // false = 大端序
// 在偏移 4 处写入 32 位整数(小端序)view.setInt32(4, 0x12345678, true); // true = 小端序
// 在偏移 8 处写入 64 位浮点数view.setFloat64(8, 3.14159, true);
// 读取console.log(view.getInt32(0, false).toString(16)); // '12345678'console.log(view.getInt32(4, true).toString(16)); // '12345678'3.2 DataView 方法一览
| 方法 | 字节数 | 描述 |
|---|---|---|
getInt8(offset) | 1 | 读取有符号 8 位整数 |
getUint8(offset) | 1 | 读取无符号 8 位整数 |
getInt16(offset, littleEndian) | 2 | 读取有符号 16 位整数 |
getUint16(offset, littleEndian) | 2 | 读取无符号 16 位整数 |
getInt32(offset, littleEndian) | 4 | 读取有符号 32 位整数 |
getUint32(offset, littleEndian) | 4 | 读取无符号 32 位整数 |
getFloat32(offset, littleEndian) | 4 | 读取 32 位浮点数 |
getFloat64(offset, littleEndian) | 8 | 读取 64 位浮点数 |
getBigInt64(offset, littleEndian) | 8 | 读取有符号 64 位 BigInt |
getBigUint64(offset, littleEndian) | 8 | 读取无符号 64 位 BigInt |
setInt8(offset, value) | 1 | 写入有符号 8 位整数 |
setUint8(offset, value) | 1 | 写入无符号 8 位整数 |
setInt16(offset, value, littleEndian) | 2 | 写入有符号 16 位整数 |
setUint16(offset, value, littleEndian) | 2 | 写入无符号 16 位整数 |
setInt32(offset, value, littleEndian) | 4 | 写入有符号 32 位整数 |
setUint32(offset, value, littleEndian) | 4 | 写入无符号 32 位整数 |
setFloat32(offset, value, littleEndian) | 4 | 写入 32 位浮点数 |
setFloat64(offset, value, littleEndian) | 8 | 写入 64 位浮点数 |
setBigInt64(offset, value, littleEndian) | 8 | 写入有符号 64 位 BigInt |
setBigUint64(offset, value, littleEndian) | 8 | 写入无符号 64 位 BigInt |
3.3 解析二进制协议示例
// 模拟一个二进制协议头// 格式:magic(4) + version(2) + flags(2) + length(4) + timestamp(8)const buffer = new ArrayBuffer(20);const view = new DataView(buffer);
// 写入协议头view.setUint32(0, 0x89504E47, false); // magic (大端序)view.setUint16(4, 1, false); // versionview.setUint16(6, 0x0001, false); // flagsview.setUint32(8, 1024, false); // lengthview.setBigUint64(12, BigInt(Date.now()), false); // timestamp
// 解析协议头function parseHeader(buffer) { const view = new DataView(buffer); return { magic: view.getUint32(0, false).toString(16), version: view.getUint16(4, false), flags: view.getUint16(6, false), length: view.getUint32(8, false), timestamp: Number(view.getBigUint64(12, false)) };}
console.log(parseHeader(buffer));四、位操作符详解
4.1 位操作符一览
| 操作符 | 名称 | 描述 |
|---|---|---|
& | 按位与 | 两位都为 1 时结果为 1 |
| ` | ` | 按位或 |
^ | 按位异或 | 两位不同时结果为 1 |
~ | 按位非 | 翻转所有位 |
<< | 左移 | 左移 n 位,低位补 0 |
>> | 算术右移 | 右移 n 位,高位补符号位 |
>>> | 无符号右移 | 右移 n 位,高位补 0 |
4.2 按位与(&)
// 提取特定位const value = 0b11010110;
// 提取低 4 位console.log((value & 0b1111).toString(2)); // '110'
// 提取第 3 位console.log((value & 0b00100000) !== 0); // true
// 判断奇偶console.log((5 & 1) === 1); // true (奇数)console.log((4 & 1) === 0); // true (偶数)
// 掩码操作const permissions = 0b1011; // 读、写、执行const READ = 0b0001;const WRITE = 0b0010;const EXECUTE = 0b1000;
console.log(permissions & READ); // 1 (有读权限)console.log(permissions & WRITE); // 2 (有写权限)console.log(permissions & EXECUTE); // 8 (有执行权限)4.3 按位或(|)
// 设置特定位let flags = 0b0000;
flags |= 0b0001; // 设置第 0 位flags |= 0b1000; // 设置第 3 位
console.log(flags.toString(2)); // '1001'
// 合并值const red = 0xFF0000;const green = 0x00FF00;const blue = 0x0000FF;
const white = red | green | blue; // 0xFFFFFF4.4 按位异或(^)
// 翻转特定位let value = 0b1010;value ^= 0b1111; // 翻转所有位console.log(value.toString(2)); // '101'
// 简单加密const key = 0x55;const data = 0x41; // 'A'const encrypted = data ^ key; // 0x14const decrypted = encrypted ^ key; // 0x41
// 交换两个数(不使用临时变量)let a = 5, b = 3;a ^= b;b ^= a;a ^= b;console.log(a, b); // 3, 54.5 按位非(~)
// 按位非的结果 = -(x + 1)console.log(~0); // -1console.log(~(-1)); // 0console.log(~5); // -6
// 快速判断数组中是否存在元素const arr = [1, 2, 3, 4, 5];const index = arr.indexOf(3);if (~index) { // index >= 0 时 ~index 为真 console.log('找到了');}
// 提取最低位的 1const x = 0b101000;const lowestBit = x & -x; // 或 x & (~x + 1)console.log(lowestBit.toString(2)); // '1000'4.6 左移(<<)
// 乘以 2 的 n 次方console.log(5 << 1); // 10 (5 * 2)console.log(5 << 2); // 20 (5 * 4)console.log(5 << 3); // 40 (5 * 8)
// 组合字节const high = 0x12;const low = 0x34;const combined = (high << 8) | low; // 0x1234
// 构造掩码const mask = 1 << 5; // 0b1000004.7 算术右移(>>)
// 除以 2 的 n 次方(保留符号)console.log(8 >> 1); // 4console.log(8 >> 2); // 2console.log(-8 >> 1); // -4 (保留符号位)
// 提取高位字节const value = 0x1234;const highByte = value >> 8; // 0x124.8 无符号右移(>>>)
// 无符号右移(高位补 0)console.log(-8 >>> 1); // 2147483644 (不是 -4!)
// 将 32 位有符号数转为无符号数const signed = -1;const unsigned = signed >>> 0; // 4294967295
// 限制为 32 位无符号整数function toUint32(x) { return x >>> 0;}
console.log(toUint32(-1)); // 4294967295console.log(toUint32(0xFFFFFFFF)); // 4294967295console.log(toUint32(0x1FFFFFFFF)); // 4294967295 (截断高32位)五、实用技巧与模式
5.1 字符串与 ArrayBuffer 互转
// 字符串 → Uint8Array(UTF-8 编码)function stringToUint8Array(str) { return new TextEncoder().encode(str);}
// Uint8Array → 字符串(UTF-8 解码)function uint8ArrayToString(uint8) { return new TextDecoder().decode(uint8);}
// 示例const str = "Hello, 世界!";const bytes = stringToUint8Array(str);console.log(bytes);// Uint8Array(13) [72, 101, 108, 108, 111, 44, 32, 228, 184, 150, 231, 149, 140]
const decoded = uint8ArrayToString(bytes);console.log(decoded); // "Hello, 世界!"5.2 十六进制字符串与 ArrayBuffer 互转
// ArrayBuffer → 十六进制字符串function bufferToHex(buffer) { return Array.from(new Uint8Array(buffer)) .map(b => b.toString(16).padStart(2, '0')) .join('');}
// 十六进制字符串 → ArrayBufferfunction hexToBuffer(hex) { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substr(i, 2), 16); } return bytes.buffer;}
// 示例const buffer = new ArrayBuffer(4);new Uint8Array(buffer).set([0xDE, 0xAD, 0xBE, 0xEF]);console.log(bufferToHex(buffer)); // 'deadbeef'
const restored = hexToBuffer('deadbeef');console.log(new Uint8Array(restored)); // Uint8Array(4) [222, 173, 190, 239]5.3 Base64 与 ArrayBuffer 互转
// ArrayBuffer → Base64function bufferToBase64(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary);}
// Base64 → ArrayBufferfunction base64ToBuffer(base64) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer;}
// 示例const buffer = new ArrayBuffer(4);new Uint8Array(buffer).set([72, 101, 108, 108]);console.log(bufferToBase64(buffer)); // 'SGVsbA=='5.4 类型转换技巧
// 浮点数与整数的底层表示转换function floatToBytes(num) { const buffer = new ArrayBuffer(8); new Float64Array(buffer)[0] = num; return new Uint8Array(buffer);}
function bytesToFloat(bytes) { const buffer = new ArrayBuffer(8); new Uint8Array(buffer).set(bytes); return new Float64Array(buffer)[0];}
// 示例const bytes = floatToBytes(3.14159);console.log(bytes);// Uint8Array(8) [82, 232, 25, 26, 133, 235, 9, 64]
console.log(bytesToFloat(bytes)); // 3.14159
// 32 位整数与浮点数互转function intBitsToFloat(bits) { const buffer = new ArrayBuffer(4); new Uint32Array(buffer)[0] = bits; return new Float32Array(buffer)[0];}
function floatToIntBits(num) { const buffer = new ArrayBuffer(4); new Float32Array(buffer)[0] = num; return new Uint32Array(buffer)[0];}5.5 字节序转换
// 翻转字节序(大端 ↔ 小端)function swapEndian16(value) { return ((value & 0xFF) << 8) | ((value >> 8) & 0xFF);}
function swapEndian32(value) { return ((value & 0xFF) << 24) | ((value & 0xFF00) << 8) | ((value >> 8) & 0xFF00) | ((value >> 24) & 0xFF);}
// 示例console.log(swapEndian16(0x1234).toString(16)); // '3412'console.log(swapEndian32(0x12345678).toString(16)); // '78563412'
// 使用 DataView 处理字节序function readLittleEndianUint32(buffer, offset) { const view = new DataView(buffer); return view.getUint32(offset, true); // true = 小端序}
function readBigEndianUint32(buffer, offset) { const view = new DataView(buffer); return view.getUint32(offset, false); // false = 大端序}5.6 位域操作
// 提取位域function extractBits(value, start, length) { const mask = (1 << length) - 1; return (value >>> start) & mask;}
// 设置位域function setBits(value, start, length, newValue) { const mask = (1 << length) - 1; return (value & ~(mask << start)) | ((newValue & mask) << start);}
// 示例const value = 0b11010110;console.log(extractBits(value, 2, 3).toString(2)); // '101'
const modified = setBits(value, 2, 3, 0b111);console.log(modified.toString(2)); // '11011110'六、逆向中的常见应用
6.1 解析加密数据
// 常见的加密数据结构function parseEncryptedData(buffer) { const view = new DataView(buffer); const result = {};
// 读取魔数 result.magic = view.getUint32(0, false);
// 读取版本 result.version = view.getUint16(4, false);
// 读取标志位 result.flags = view.getUint16(6, false);
// 读取数据长度 result.length = view.getUint32(8, false);
// 读取时间戳 result.timestamp = Number(view.getBigUint64(12, false));
// 读取数据 result.data = new Uint8Array(buffer, 20, result.length);
return result;}6.2 模拟 Hash 算法
// 简单的循环移位function rotateLeft(value, shift, bits = 32) { shift = shift % bits; return ((value << shift) | (value >>> (bits - shift))) >>> 0;}
function rotateRight(value, shift, bits = 32) { shift = shift % bits; return ((value >>> shift) | (value << (bits - shift))) >>> 0;}
// 示例console.log(rotateLeft(0x12345678, 4).toString(16)); // '23456781'console.log(rotateRight(0x12345678, 4).toString(16)); // '81234567'6.3 分析混淆代码
// 常见的混淆模式:字符串数组索引const stringArray = ['log', 'warn', 'error', 'info'];const indexMap = { 0x0: 0, 0x1: 1, 0x2: 2, 0x3: 3 };
function getString(encodedIndex) { return stringArray[indexMap[encodedIndex]];}
// 常见的混淆模式:位运算加密function decryptChar(encrypted, key) { return String.fromCharCode(encrypted ^ key);}
// 分析时识别这些模式// _0x1234 ^ _0x5678 → 异或解密// _0x1234 << 2 → 左移乘法// _0x1234 >> 1 → 右移除法// _0x1234 & 0xFF → 取低字节七、性能优化建议
7.1 避免频繁创建视图
// 不好的做法for (let i = 0; i < 1000; i++) { const view = new DataView(buffer); view.setInt32(0, i);}
// 好的做法const view = new DataView(buffer);for (let i = 0; i < 1000; i++) { view.setInt32(0, i);}7.2 使用 slice 而非 copy
// 不好的做法:手动复制function copyBytes(buffer, start, length) { const result = new Uint8Array(length); const source = new Uint8Array(buffer); for (let i = 0; i < length; i++) { result[i] = source[start + i]; } return result;}
// 好的做法:使用 slicefunction sliceBytes(buffer, start, length) { return new Uint8Array(buffer, start, length).slice();}7.3 批量操作
// 不好的做法:逐字节操作for (let i = 0; i < data.length; i++) { view.setUint8(i, data[i]);}
// 好的做法:批量设置new Uint8Array(buffer).set(data);八、常见陷阱与注意事项
8.1 TypedArray 长度计算
const buffer = new ArrayBuffer(16);
// TypedArray 长度 = buffer 字节数 / 每个元素字节数console.log(new Uint8Array(buffer).length); // 16console.log(new Uint16Array(buffer).length); // 8console.log(new Uint32Array(buffer).length); // 4console.log(new Float64Array(buffer).length);// 2
// 带偏移和长度的视图const view = new Uint8Array(buffer, 4, 8); // 从偏移 4 开始,长度 8console.log(view.length); // 88.2 溢出行为
const uint8 = new Uint8Array(1);const int8 = new Int8Array(1);
// Uint8Array: 溢出时取模uint8[0] = 256; // 256 % 256 = 0console.log(uint8[0]); // 0
uint8[0] = -1; // -1 & 0xFF = 255console.log(uint8[0]); // 255
// Int8Array: 溢出时取模并解释为有符号数int8[0] = 128; // 128 % 256 = 128 → 解释为 -128console.log(int8[0]); // -1288.3 字节序陷阱
// 平台字节序检测const isLittleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;console.log(isLittleEndian ? '小端序' : '大端序');
// 网络协议通常使用大端序// x86/ARM 通常使用小端序// 使用 DataView 明确指定字节序可以避免问题8.4 内存对齐
// 某些平台要求内存对齐const buffer = new ArrayBuffer(16);const uint32 = new Uint32Array(buffer, 1); // 偏移 1 字节(未对齐)// 在某些平台上可能导致性能下降或错误
// 建议:使用 4 或 8 字节对齐的偏移const aligned = new Uint32Array(buffer, 4); // 偏移 4 字节(对齐)九、总结
| 概念 | 用途 |
|---|---|
ArrayBuffer | 原始二进制数据容器 |
TypedArray | 类型化数组视图,快速读写特定类型 |
DataView | 灵活的字节操作,支持字节序控制 |
Uint8Array | 字节级操作,逆向最常用 |
| 位操作符 | 底层数据处理、加密算法 |
选择建议:
- 处理字节数据 →
Uint8Array - 需要控制字节序 →
DataView - 处理二进制协议 →
DataView+Uint8Array - 加密算法实现 → 位操作符 +
Uint32Array
参考资料
部分信息可能已经过时









