了解位、字節和數字基數
我最近的任務是創建一個簡單的命令行程序,該程序將輸入一個未知內容的文件並打印一個十六進制轉儲作為輸出。但是,我並不真正知道如何訪問文件的數據,也不知道十六進制轉儲是什麼。因此,我將與您分享我學到的東西以及為完成這項任務而寫的東西。
由於我最熟悉 JavaScript,因此我決定在 Node.js 中執行此操作。目的是編寫這樣的命令:
node hexdump.js data
這將運行 hexdump.js
文件上的程序(data
) 並輸出十六進制轉儲。
該文件可以是任何東西——圖像、二進製文件、常規文本文件或具有其他編碼數據的文件。在我的特殊情況下,它是一個 ROM。
如果您曾經嘗試使用文本編輯器打開非基於文本的文件,您會記得看到亂七八糟的隨機字符。如果您想知道程序如何訪問原始數據並使用它,這篇文章可能會有所啟發。
本文將由兩部分組成:第一,背景信息解釋什麼是十六進制轉儲,什麼是位和字節,如何計算以 2、10 和 16 為底的值,以及對可打印 ASCII 字符的解釋。第二部分將在Node中編寫十六進制轉儲函數。
什麼是十六進制轉儲?
要了解什麼是十六進制轉儲,我們可以創建一個文件並查看它的十六進制轉儲。我將製作一個包含 Bob Ross 名言的簡單文本文件。
echo -en "Just make a decision and let it go." > data
-en
這是防止尾隨換行符並允許解釋反斜杠轉義字符,這將派上用場。另外,data
只是一個文件名,不是任何命令或關鍵字。
Unix 系統已經有一個 hexdump 命令,我將使用規範的 (-C
) 標誌來格式化輸出。
hexdump -C data
這是我得到的。
00000000 4a 75 73 74 20 6d 61 6b 65 20 61 20 64 65 63 69 |Just make a deci|
00000010 73 69 6f 6e 20 61 6e 64 20 6c 65 74 20 69 74 20 |sion and let it |
00000020 67 6f 2e |go.|
00000023
好的,看起來我有一堆數字,在右邊我們可以看到我剛剛回顯的字符串中的文本字符。手冊頁告訴我們 hexdump
“以十六進制、十進制、八進製或 ascii 顯示文件內容”。這裡使用的具體格式(規範)進一步說明:
規範的十六進制+ASCII 顯示。以十六進制顯示輸入偏移量,後跟 16 個空格分隔的兩列十六進製字節,然後是 %_p 中相同的 16 個字節 '| 中包含的格式 ' 字符。
所以現在我們可以看到每一行都是一個十六進制輸入偏移量(地址),有點像行號,後面跟著 16 個十六進製字節,然後是兩個管道之間的 ASCII 格式的相同字節。
地址 | 十六進製字節 | ASCII |
---|---|---|
00000000 | 4a 75 73 74 20 6d 61 6b 65 20 61 20 64 65 63 69 | Just make a deci |
00000010 | 73 69 6f 6e 20 61 6e 64 20 6c 65 74 20 69 74 20 | sion and let it |
00000020 | 67 6f 2e | go. |
00000023 |
首先,讓我們看一下輸入偏移量,也稱為地址。我們可以看到它有前導零和一個數字。例如,在文本編輯器中,我們有十進制的代碼行,加一。第 1 行,第 2 行,一直到第 382 行,或者程序有多長。
十六進制轉儲計數的地址跟踪數據中的字節數,並將每行偏移該數字。所以第一行從偏移量 0 開始,第二行代表數字 16,即當前行之前的字節數。 10
是 16
十六進制,我們將在本文中進一步介紹。
接下來我們有ASCII。如果您不熟悉,ASCII 是一種字符編碼標準。它將控製字符和可打印字符與數字匹配。這是一個完整的 ASCII 表。
現在這種十六進制轉儲對於查看 ASCII 文本是有意義的,但是對於不能用 ASCII 表示的數據呢?不是每個字節或數字都有一個 ASCII 匹配,那麼看起來如何?
在另一個示例中,我將回顯 0-15 以基數 16/十六進製表示,即 00
到 0f
.使用 echo
轉義十六進制數字 , 數字前必須有 \x
.
echo -en "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" > data2
這些數字不對應任何 ASCII 字符,也無法在常規文本編輯器中查看。例如,如果您嘗試在 VSCode 中打開它,您會看到“該文件未顯示在編輯器中,因為它是二進製文件或使用了不受支持的文本編碼。”。
如果您決定打開它,您可能會看到一個問號。幸運的是,我們可以使用 hexdump 查看原始內容。
00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
00000010
如您所見,不可打印的 ASCII 字符由 .
表示 ,並且字節被確認為十六進制。地址有10
在第二行,因為它從第 16 個字節開始,而 16 是 10
十六進制。
理解字節和基數
查看 hexdump
的“十六進製字節”部分 表,你必須知道“十六進制”是什麼意思,“字節”是什麼。
您可能已經知道一千字節大約是一千字節,或 1024
字節,一兆字節大約是一千千字節,或 1,024 * 1,024
字節(1,048,576
字節),或者甚至軟盤有 1,474,560
字節的存儲空間。
但是字節到底是什麼?
位、半字節和字節
位是二進制數字,計算機上最小的數據形式,可以是0
或 1
.和布爾值一樣,一個位可以表示開/關、真/假等。一個半字節有四位,一個字節有八位。
單位 | 存儲 |
---|---|
位 | 二進制數(0 或 1 ) |
輕咬 | 4 位 |
字節 | 8 位 |
計算機以字節為單位處理數據。
一個字節的值
你有沒有玩過一個視頻遊戲,在 255
?為什麼到了那個時候就停了?
如果遊戲中每個庫存存儲使用一個字節,那麼可以表示的最高值是多少?
使用二進制(以 2 為底的數字系統)最容易對此進行評估。一個字節有 8 個 1 位槽。因為我們知道一個位的最高值是 1
, 最高二進制 8 位值必須是 8 1
s - 11111111
.
二進制:111111112
我們怎麼知道11111111
代表數字255
(十進制)?我們將手動計算該值。
在基本系統中,每個數字的權重都不同。在十進制中,7
中的七 vs 70
不代表相同的值。我將首先以二進制形式演示,然後以十進制和十六進制演示。
從最低有效值(一直到右邊的那個)開始,您必須將每個數字乘以基數提升到其位置的結果,然後將它們加在一起。一直到右邊的位置是 0,然後是 1,然後是 2,以此類推,直到結束,在本例中為 7。
看起來是這樣的:
1 * 2 ** 7 +
1 * 2 ** 6 +
1 * 2 ** 5 +
1 * 2 ** 4 +
1 * 2 ** 3 +
1 * 2 ** 2 +
1 * 2 ** 1 +
1 * 2 ** 0 = 255
在計算指數之後,你可以寫出這樣的方程:
1 * 128 + 1 * 64 + 1 * 32 + 1 * 16 + 1 * 8 + 1 * 4 + 1 * 2 + 1 * 1 = 255
或者簡單地說:
128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
舉個更簡單的例子,如果號碼是 101
應該是:
1 * 2 ** 2 + 0 * 2 ** 1 + 1 * 2 ** 0 = 5
十進制:25510
如果這沒有意義,請以十進制考慮。你知道007
和 070
和 700
都是非常不同的值(前導零對值沒有影響)。七是7 * 10^0
, 七十是 7 * 10^1
,七百是 7 * 10^2
.
數字 | 十進製表示 | 計算 |
---|---|---|
七 | 007 | 7 * 10^0 或 7 * 1 |
七十 | 070 | 7 * 10^1 或 7 * 10 |
七百 | 700 | 7 * 10^2 或 7 * 100 |
所以我們可以看到,數字的位置決定了數值,我們可以用同樣的計算得到255
十進制。
2 * 10 ** 2 + 5 * 10 ** 1 + 5 * 10 ** 0 = 255
或者:
2 * 100 + 5 * 10 + 5 * 1 = 255
或者:
200 + 50 + 5 = 255
十六進制:FF16
這個概念適用於任何基地。十六進制以 16 為底,F
代表最大值,15
.
15 * 16 ** 1 + 15 * 16 ** 0 = 255
或者:
15 * 16 + 15 * 1 = 255
或者:
240 + 15 = 255
都是同一個數字
這裡要考慮的重要概念是 11111111
, 255
, 和 FF
都代表同一個數字,我們很難直觀地意識到這一點,因為我們已經習慣了以 10 為基數。這個數字也恰好是一個字節的最大值。
十六進制是一種方便、緊湊的方式來表示一個字節的值,因為它總是包含在兩個字符中。
// Binary - 11111111
1 * 2 ** 7 +
1 * 2 ** 6 +
1 * 2 ** 5 +
1 * 2 ** 4 +
1 * 2 ** 3 +
1 * 2 ** 2 +
1 * 2 ** 1 +
1 * 2 ** 0
// Decimal - 255
2 * 10 ** 2 + 5 * 10 ** 1 + 5 * 10 ** 0
// Hexadecimal - FF
15 * 16 ** 1 + 15 * 16 ** 0
在編程中表示不同的bases
編程語言將使用前綴來表示基數 10 之外的值。二進制是 0b
, 十六進制是 0x
,所以你可以寫 0b1111
或 0xff
例如,在 Node repl 中,它將以十進制輸出值。
基礎 | 前綴 |
---|---|
二進制 | 0b |
十六進制 | 0x |
八進制是另一個基本系統,以 8 為基數,僅由前導 0
表示 或 0o
.
010 === 8 // true
不過,在本文中,我們將主要忽略八進制。
不同基數計數
一個字節的最大值是255
,一個半字節(4位)的最大值是15
.這是一個計數到 15
的圖表 二進制、十進制和十六進制。
二進制(以 2 為底) | 十進制(以 10 為底) | 十六進制(以 16 為基數) |
---|---|---|
0000 | 0 | 00 |
0001 | 1 | 01 |
0010 | 2 | 02 |
0011 | 3 | 03 |
0100 | 4 | 04 |
0101 | 5 | 05 |
0110 | 6 | 06 |
0111 | 7 | 07 |
1000 | 8 | 08 |
1001 | 9 | 09 |
1010 | 10 | 0a |
1011 | 11 | 0b |
1100 | 12 | 0c |
1101 | 13 | 0d |
1110 | 14 | 0e |
1111 | 15 | 0f |
十六進制通常用前導零編寫,使得一個字節的表示總是兩個字符。
所以現在我們應該對十六進制轉儲的地址和字節中表示的值有一個很好的了解。
可打印的 ASCII 字符
0x20
之間 和 0x7e
都是可打印的 ASCII 字符。此圖表顯示了它們,以及它們的二進制、八進制、十進制和十六進制對應項。在 hexdump
上面的例子,我打印了 0x00
到 0x0f
,並且由於這些都不是可打印的 ASCII 字符,因此它們顯示為點。
用 JavaScript 編寫十六進制轉儲
現在回到在 Node.js 中編寫十六進制轉儲程序的原始任務。我們知道它應該是什麼樣子,並且我們了解原始數據的價值,但是從哪裡開始呢?
好吧,我們知道我們希望程序如何運行。它應該能夠使用文件名作為參數和 console.log
十六進制轉儲。
node hexdump.js data
所以很明顯我會製作 hexdump.js
我還會製作一些包含可打印和不可打印 ASCII 字符的新數據。
echo -en "<blink>Talent is pursued interest</blink>\x00\xff" > data
目標是做出這個輸出:
00000000 3c 62 6c 69 6e 6b 3e 54 61 6c 65 6e 74 20 69 73 |<blink>Talent is|
00000010 20 70 75 72 73 75 65 64 20 69 6e 74 65 72 65 73 | pursued interes|
00000020 74 3c 2f 62 6c 69 6e 6b 3e 00 ff |t</blink>..|
0000002b
獲取文件的原始數據緩衝區
第一步是以某種方式從文件中獲取數據。我將從使用文件系統模塊開始。
const fs = require('fs')
為了獲得文件名,我們只需要獲得第三個命令行參數(0
作為 Node 二進製文件,1
是 hexdump.js
, 和 2
是 data
)。
const filename = process.argv.slice(2)[0]
我將使用 readFile()
獲取文件的內容。 (readFileSync()
只是同步版本。)正如 API 所說,“如果沒有指定編碼,則返回原始緩衝區”,所以我們得到了一個緩衝區。 (utf8
是我們用於字符串的。)
function hexdump(filename) {
let buffer = fs.readFileSync(filename)
return buffer
}
console.log(hexdump(filename))
這將註銷 <Buffer>
對象(為簡潔起見刪除了值)。
<Buffer 3c 62 6c 69 6e 6b 3e 54 ... 69 6e 6b 3e 00 ff>
好吧,這看起來很熟悉。感謝所有這些背景知識,我們可以看到緩衝區是一堆以十六進製表示的字節。你甚至可以看到最後的 00
和 ff
我在裡面附和了。
使用緩衝區
您可以將緩衝區視為數組。如果您使用 buffer.length
檢查長度 ,你會得到 43
,它對應於字節數。由於我們需要 16 字節的行,我們可以遍歷每 16 個字節並將它們分割成塊。
function hexdump(filename) {
let buffer = fs.readFileSync(filename)
let lines = []
for (let i = 0; i < buffer.length; i += 16) {
let block = buffer.slice(i, i + 16) // cut buffer into blocks of 16
lines.push(block)
}
return lines
}
現在我們有了一個更小的緩衝區數組。
[ <Buffer 3c 62 6c 69 6e 6b 3e 54 61 6c 65 6e 74 20 69 73>,
<Buffer 20 70 75 72 73 75 65 64 20 69 6e 74 65 72 65 73>,
<Buffer 74 3c 2f 62 6c 69 6e 6b 3e 00 ff> ]
計算地址
我們想用十六進製表示地址,你可以用 toString(16)
將數字轉換為十六進製字符串 .然後我會在前面加上一些零,所以它總是相同的長度。
let address = i.toString(16).padStart(8, '0')
那麼如果我將地址和塊放在模板字符串中會發生什麼?
lines.push(`${address} ${block}`)
[ '00000000 <blink>Talent is',
'00000010 pursued interes',
'00000020 t</blink>\u0000?' ]
模板嘗試將緩衝區轉換為字符串。它不會按照我們想要的方式解釋不可打印的 ASCII 字符,因此我們無法對 ASCII 輸出執行此操作。不過,我們現在有了正確的地址。
創建 hex 和 ASCII 字符串
當您訪問緩衝區中的每個值時,它會將其解釋為原始數字,您是否選擇將其表示為二進制、十六進制、ASCII 或其他任何內容取決於您。我將創建一個十六進制數組和一個 ASCII 數組,然後將它們連接成字符串。這樣,模板文字就已經有一個字符串表示可以使用了。
為了得到 ASCII 字符,我們可以根據上面的可打印 ASCII 圖表來測試值 - >= 0x20
和 < 0x7f
- 然後獲取字符代碼或點。獲取十六進制值與地址相同 - 將其轉換為 base 16 字符串並使用 0
填充單個值 .
我將在該行中添加一些空格並將這些行轉換為換行符分隔的字符串。
function hexdump(filename) {
let buffer = fs.readFileSync(filename)
let lines = []
for (let i = 0; i < buffer.length; i += 16) {
let address = i.toString(16).padStart(8, '0') // address
let block = buffer.slice(i, i + 16) // cut buffer into blocks of 16
let hexArray = []
let asciiArray = []
for (let value of block) {
hexArray.push(value.toString(16).padStart(2, '0'))
asciiArray.push(
value >= 0x20 && value < 0x7f ? String.fromCharCode(value) : '.'
)
}
let hexString = hexArray.join(' ')
let asciiString = asciiArray.join('')
lines.push(`${address} ${hexString} |${asciiString}|`)
}
return lines.join('\n')
}
現在我們快到了。
00000000 3c 62 6c 69 6e 6b 3e 54 61 6c 65 6e 74 20 69 73 |<blink>Talent is|
00000010 20 70 75 72 73 75 65 64 20 69 6e 74 65 72 65 73 | pursued interes|
00000020 74 3c 2f 62 6c 69 6e 6b 3e 00 ff |t</blink>..|
完整的十六進制轉儲程序
在這一點上唯一剩下的是一些最終格式 - 如果少於 16 個字節,則在最後一行添加填充,並將字節分成兩個 8 個塊,這對我來說不太重要,無法解釋。
這是最終版本的要點,或見下文。
hexdump.jsconst fs = require('fs')
const filename = process.argv.slice(2)[0]
function hexdump(filename) {
let buffer = fs.readFileSync(filename)
let lines = []
for (let i = 0; i < buffer.length; i += 16) {
let address = i.toString(16).padStart(8, '0') // address
let block = buffer.slice(i, i + 16) // cut buffer into blocks of 16
let hexArray = []
let asciiArray = []
let padding = ''
for (let value of block) {
hexArray.push(value.toString(16).padStart(2, '0'))
asciiArray.push(
value >= 0x20 && value < 0x7f ? String.fromCharCode(value) : '.'
)
}
// if block is less than 16 bytes, calculate remaining space
if (hexArray.length < 16) {
let space = 16 - hexArray.length
padding = ' '.repeat(space * 2 + space + (hexArray.length < 9 ? 1 : 0)) // calculate extra space if 8 or less
}
let hexString =
hexArray.length > 8
? hexArray.slice(0, 8).join(' ') + ' ' + hexArray.slice(8).join(' ')
: hexArray.join(' ')
let asciiString = asciiArray.join('')
let line = `${address} ${hexString} ${padding}|${asciiString}|`
lines.push(line)
}
return lines.join('\n')
}
console.log(hexdump(filename))
正如我之前提到的,您希望將可讀流用於真正的十六進制轉儲程序,但這是一個很好的開始示例。以後我可能會用改進的版本更新這篇文章。
結論
我在這篇文章中介紹了很多概念。
- 位、半字節和字節
- 二進制、十進制和十六進制數
- 計算任何基本系統中的數字的值
- 可打印的 ASCII 字符
- 在 Node.js 中訪問文件數據
- 處理原始數據緩衝區 - 將數字轉換為十六進制和 ASCII
關於這個主題我還有更多想寫的,比如創建一個 16 位十六進制轉儲、按位運算符和字節序,以及使用 Streams 來改進這個十六進制轉儲功能,所以後續可能會有更多內容文章。
我在這裡學到的一切都歸功於 Vanya Sergeev。任何誤導性的數據或低效的代碼都是我自己的。