目錄

名稱

perlpacktut - packunpack 的教學

說明

packunpack 是兩個用於根據使用者定義的範本轉換資料的函數,在 Perl 儲存值的受保護方式和 Perl 程式環境中可能需要的某些明確定義的表示方式之間轉換。遺憾的是,它們也是 Perl 提供的最令人誤解和最常被忽略的兩個函數。本教學將為您消除它們的神秘感。

基本原則

大多數程式語言都不會保護儲存變數的記憶體。例如,在 C 中,您可以取得某些變數的位址,而 sizeof 營運子會告訴您分配給變數的位元組數。使用位址和大小,您可以隨心所欲地存取儲存空間。

在 Perl 中,您無法隨機存取記憶體,但 packunpack 提供的結構和表示轉換是一種極佳的替代方案。pack 函數會將值轉換成包含根據給定規格(即所謂的「範本」引數)的表示的位元組序列。unpack 是反向程序,從位元組字串的內容中衍生出一些值。(不過,請注意,並非所有已封裝在一起的內容都可以順利解封裝 - 經驗豐富的旅客可能會證實這是一個非常普遍的經驗。)

您可能會問,為什麼您需要一個包含某些值(以二進位表示)的記憶體區塊?一個很好的理由是輸入和輸出存取某些檔案、裝置或網路連線,這種二進位表示法可能會強加於您,或會在處理中為您帶來一些好處。另一個原因是將資料傳遞給無法作為 Perl 函數使用的某些系統呼叫:syscall 要求您提供以 C 程式中發生方式儲存的參數。即使是文字處理(如下一節所示)也可以透過明智地使用這兩個函數來簡化。

要了解 (un)packing 的運作方式,我們將從一個簡單的範本程式碼開始,其中轉換處於低檔:位元組序列的內容與十六進位數字字串之間。讓我們使用 unpack,因為這可能會讓您想起一個傾印程式,或一些不幸的程式在它們過期進入廣闊的藍天之前習慣於向您發送的絕望最後訊息。假設變數 $mem 保留了一個位元組序列,我們希望檢查它而不假設其含義,我們可以撰寫

my( $hex ) = unpack( 'H*', $mem );
print "$hex\n";

然後我們可能會看到類似這樣的東西,其中每對十六進位數字對應一個位元組

41204d414e204120504c414e20412043414e414c2050414e414d41

這段記憶體中是什麼?數字、字元,還是兩者的混合?假設我們在使用 ASCII (或類似) 編碼的電腦上:十六進位值在範圍 0x40 - 0x5A 表示一個大寫字母,而 0x20 編碼一個空格。因此,我們可以假設它是一段文字,有些人可以像小報一樣閱讀它;但其他人必須取得一個 ASCII 表,並重溫一年級生的感覺。不太關心以哪種方式閱讀它,我們注意到具有範本程式碼 Hunpack 將位元組序列的內容轉換為慣用的十六進位符號。由於「一個序列」是一個相當模糊的數量指示,因此 H 已被定義為僅轉換一個十六進位數字,除非後面跟著一個重複計數。重複計數的星號表示使用任何剩餘的。

反向操作 - 從十六進位數字字串封裝位元組內容 - 同樣容易撰寫。例如

my $s = pack( 'H2' x 10, 30..39 );
print "$s\n";

由於我們將一個包含十個 2 位數十六進位字串的清單提供給 pack,因此封裝範本應包含十個封裝程式碼。如果這在具有 ASCII 字元編碼的電腦上執行,它將列印 0123456789

封裝文字

假設您必須讀取類似這樣的資料檔案

Date      |Description                | Income|Expenditure
01/24/2001 Zed's Camel Emporium                    1147.99
01/28/2001 Flea spray                                24.99
01/29/2001 Camel rides to tourists      235.00

我們如何做到這一點?您可能首先會想到使用 split;然而,由於 split 會壓縮空白欄位,因此您永遠不會知道記錄是收入還是支出。糟糕。嗯,您隨時可以使用 substr

while (<>) { 
    my $date   = substr($_,  0, 11);
    my $desc   = substr($_, 12, 27);
    my $income = substr($_, 40,  7);
    my $expend = substr($_, 52,  7);
    ...
}

這一點也不好笑,對吧?事實上,它比看起來的還要糟;目光銳利的人可能會注意到第一個欄位應該只有 10 個字元寬,而錯誤已傳播到其他數字,我們必須手動計算這些數字。因此,它容易出錯,而且非常不友善。

或者我們可以使用正規表示式

while (<>) { 
    my($date, $desc, $income, $expend) = 
        m|(\d\d/\d\d/\d{4}) (.{27}) (.{7})(.*)|;
    ...
}

呃。嗯,它好一點,但是,你會想要維護它嗎?

嘿,Perl 不應該讓這類事情變簡單嗎?嗯,如果你使用正確的工具,它會的。packunpack 是在處理上述類型的固定寬度資料時,協助你的設計。讓我們看看使用 unpack 的解決方案

while (<>) { 
    my($date, $desc, $income, $expend) = unpack("A10xA27xA7A*", $_);
    ...
}

看起來好一點;但是我們必須拆解那個奇怪的範本。我是從哪裡弄到它的?

好的,讓我們再次看看一些我們的資料;事實上,我們會包含標頭,以及一個方便的尺規,以便我們可以追蹤我們在哪裡。

         1         2         3         4         5        
1234567890123456789012345678901234567890123456789012345678
Date      |Description                | Income|Expenditure
01/28/2001 Flea spray                                24.99
01/29/2001 Camel rides to tourists      235.00

從這裡,我們可以看到日期欄位從欄位 1 延伸到欄位 10,寬度為十個字元。pack-ese 的「字元」是 A,而十個字元是 A10。因此,如果我們只想擷取日期,我們可以這樣說

my($date) = unpack("A10", $_);

好的,接下來是什麼?在日期和說明之間有一個空白欄位;我們想要跳過它。x 範本表示「向前跳」,因此我們想要其中一個。接下來,我們有另一批字元,從 12 到 38。那是 27 個字元,因此是 A27。(不要犯柵欄錯誤,12 到 38 之間有 27 個字元,而不是 26 個。算算看!)

現在我們跳過另一個字元,並擷取下一個 7 個字元

my($date,$description,$income) = unpack("A10xA27xA7", $_);

現在是聰明的地方。我們的帳本中僅為收入而非支出的行可能會結束在欄位 46。因此,我們不想要告訴我們的 unpack 模式我們需要找到另外 12 個字元;我們只會說「如果還有剩餘,就取用它」。正如你從正規表示式中猜測的那樣,這就是 * 的意思:「使用所有剩餘的」。

因此,將所有內容放在一起

my ($date, $description, $income, $expend) =
    unpack("A10xA27xA7xA*", $_);

現在,我們的資料已剖析完畢。我想我們現在可以做的是計算我們的收入和支出,並在我們的帳本最後加上一行(格式相同),說明我們收入了多少,以及我們花了多少

while (<>) {
    my ($date, $desc, $income, $expend) =
        unpack("A10xA27xA7xA*", $_);
    $tot_income += $income;
    $tot_expend += $expend;
}

$tot_income = sprintf("%.2f", $tot_income); # Get them into 
$tot_expend = sprintf("%.2f", $tot_expend); # "financial" format

$date = POSIX::strftime("%m/%d/%Y", localtime); 

# OK, let's go:

print pack("A10xA27xA7xA*", $date, "Totals",
    $tot_income, $tot_expend);

喔,嗯。那不太對。讓我們看看發生了什麼事

01/24/2001 Zed's Camel Emporium                     1147.99
01/28/2001 Flea spray                                 24.99
01/29/2001 Camel rides to tourists     1235.00
03/23/2001Totals                     1235.001172.98

好的,這是個開始,但空格發生了什麼事?我們放了 x,不是嗎?它不應該向前跳嗎?讓我們看看 perlfunc 中的「pack」 怎麼說

x   A null byte.

噁。難怪了。「空位元組」、零字元和「空格」(32 字元)之間有很大的差異。Perl 在日期和說明之間放了一些東西,但不幸的是,我們看不到它!

我們實際上需要做的是擴充欄位的寬度。A 格式會用空格填滿任何不存在的字元,因此我們可以使用額外的空格來排列我們的欄位,如下所示

print pack("A11 A28 A8 A*", $date, "Totals",
    $tot_income, $tot_expend);

(請注意,您可以在範本中放置空格以使其更具可讀性,但它們不會轉換為輸出中的空格。)以下是我們這次得到的結果

01/24/2001 Zed's Camel Emporium                     1147.99
01/28/2001 Flea spray                                 24.99
01/29/2001 Camel rides to tourists     1235.00
03/23/2001 Totals                      1235.00 1172.98

這樣好一點了,但我們仍然有最後一欄需要進一步移動。有一個簡單的方法可以解決這個問題:不幸的是,我們無法讓 pack 右對齊我們的欄位,但我們可以讓 sprintf 這樣做

$tot_income = sprintf("%.2f", $tot_income); 
$tot_expend = sprintf("%12.2f", $tot_expend);
$date = POSIX::strftime("%m/%d/%Y", localtime); 
print pack("A11 A28 A8 A*", $date, "Totals",
    $tot_income, $tot_expend);

這次我們得到了正確的答案

01/28/2001 Flea spray                                 24.99
01/29/2001 Camel rides to tourists     1235.00
03/23/2001 Totals                      1235.00      1172.98

因此,這就是我們如何使用和產生定寬資料。讓我們回顧一下到目前為止我們對 packunpack 的了解

封裝數字

文字資料就到此為止。讓我們來看看 packunpack 最擅長的內容:處理數字的二進位格式。當然,二進位格式不只一種,如果人生這麼簡單就好了,不過 Perl 會幫你處理所有繁瑣的工作。

整數

封裝和解封數字表示轉換為某種特定二進位表示法,並從中轉換回來。暫時撇開浮點數不談,任何此類表示法的顯著屬性為

因此,例如,要將 20302 封裝到電腦表示法中的有號 16 位元組整數,你可以寫下

my $ps = pack( 's', 20302 );

再次,結果是一個字串,現在包含 2 個位元組。如果您列印此字串(通常不建議這麼做),您可能會看到 ONNO(視您的系統位元組順序而定) - 或如果您的電腦未使用 ASCII 字元編碼,則會看到完全不同的東西。使用相同的範本解壓縮 $ps 會傳回原始整數值

my( $s ) = unpack( 's', $ps );

這適用於所有數字範本代碼。但不要期待奇蹟:如果封裝值超過分配的位元組容量,高階位元會被靜默捨棄,而解壓縮肯定無法從某個魔術帽中將它們拉出來。而且,當您使用有號範本代碼(例如 s)封裝時,過多值可能會導致符號位元被設定,而解壓縮這項值將聰明地傳回負值。

16 位元組無法讓您使用整數走得太遠,但有 lL 可用於有號和無號 32 位元組整數。如果這還不夠,而且您的系統支援 64 位元組整數,您可以使用封裝代碼 qQ 將限制推得更接近無限大。封裝代碼 iI 提供了一個顯著的例外,用於有號和無號「當地自訂」類型的整數:此類整數將佔用與當地 C 編譯器傳回的 sizeof(int) 一樣多的位元組,但它將使用 至少 32 個位元組。

每個整數封裝碼 sSlLqQ 會產生固定數量的位元組,不論您在哪裡執行程式。這對某些應用程式來說可能很有用,但它無法提供在 Perl 和 C 程式之間傳遞資料結構的移植方式(當您呼叫 XS 延伸模組或 Perl 函式 syscall 時,或當您讀取或寫入二進位檔案時,就會發生這種情況)。這種情況下,您需要的是範本碼,它取決於您的本地 C 編譯器在編寫 shortunsigned long 時編譯的內容。這些碼及其對應的位元組長度顯示在以下表格中。由於 C 標準在這些資料類型的相對大小方面留有很大的迴旋餘地,因此實際值可能會有所不同,這就是為什麼這些值在 C 和 Perl 中表示為表達式的原因。(如果您想在程式中使用 %Config 中的值,您必須使用 use Config 匯入它。)

signed unsigned  byte length in C   byte length in Perl       
  s!     S!      sizeof(short)      $Config{shortsize}
  i!     I!      sizeof(int)        $Config{intsize}
  l!     L!      sizeof(long)       $Config{longsize}
  q!     Q!      sizeof(long long)  $Config{longlongsize}

i!I! 碼與 iI 沒有不同;它們被容忍,以求完整性。

解開堆疊結構

當您處理來自某些特定架構的二進位資料,而您的程式可能在完全不同的系統上執行時,可能需要請求特定的位元組順序。舉例來說,假設您有 24 個位元組包含一個堆疊結構,就像在 Intel 8086 上發生的那樣

     +---------+        +----+----+               +---------+
TOS: |   IP    |  TOS+4:| FL | FH | FLAGS  TOS+14:|   SI    |
     +---------+        +----+----+               +---------+
     |   CS    |        | AL | AH | AX            |   DI    |
     +---------+        +----+----+               +---------+
                        | BL | BH | BX            |   BP    |
                        +----+----+               +---------+
                        | CL | CH | CX            |   DS    |
                        +----+----+               +---------+
                        | DL | DH | DX            |   ES    |
                        +----+----+               +---------+

首先,我們注意到這款歷史悠久的 16 位元 CPU 使用小端序,這就是為什麼低階位元組儲存在較低位址的原因。要解開這樣的(未簽署)短整數,我們必須使用碼 v。重複計數會解開所有 12 個短整數

my( $ip, $cs, $flags, $ax, $bx, $cx, $dx, $si, $di, $bp, $ds, $es ) =
  unpack( 'v12', $frame );

或者,我們可以使用 C 來解開可個別存取的位元組暫存器 FL、FH、AL、AH 等。

my( $fl, $fh, $al, $ah, $bl, $bh, $cl, $ch, $dl, $dh ) =
  unpack( 'C10', substr( $frame, 4, 10 ) );

如果我們能一次完成所有這些操作,那就太好了:解開一個短整數,備份一點,然後解開 2 個位元組。由於 Perl 好,它提供了範本碼 X 來備份一個位元組。將所有這些放在一起,我們現在可以寫

my( $ip, $cs,
    $flags,$fl,$fh,
    $ax,$al,$ah, $bx,$bl,$bh, $cx,$cl,$ch, $dx,$dl,$dh, 
    $si, $di, $bp, $ds, $es ) =
unpack( 'v2' . ('vXXCC' x 5) . 'v5', $frame );

(可以避免範本的笨拙結構 - 繼續讀下去!)

我們費了很大的力氣來建構範本,以使其與我們的結構緩衝區的內容相符。否則,我們會得到未定義的值,或者 unpack 無法解開所有內容。如果 pack 用完項目,它將提供空字串(只要封裝碼這麼說,就會強制轉換為零)。

如何在網路上吃雞蛋

大端序(最低位元組在最低位址)的封包碼對 16 位元整數為 n,對 32 位元整數為 N。如果你知道你的資料來自相容的架構,則可以使用這些碼,但令人驚訝的是,如果你透過網路與對你幾乎一無所知的系統交換二進位資料,你也應該使用這些封包碼。簡單的理由是此順序已被選為網路順序,所有遵循標準的程式都應遵循此慣例。(這當然是對小人之國政黨的嚴厲支持,並且很可能會影響那裡的政治發展。)因此,如果通訊協定希望你透過先傳送長度,再傳送這麼多位元組的方式傳送訊息,你可以撰寫

my $buf = pack( 'N', length( $msg ) ) . $msg;

甚至

my $buf = pack( 'NA*', length( $msg ), $msg );

並將 $buf 傳遞給你的傳送常式。有些通訊協定要求計數應包含計數本身的長度:然後只需將 4 加到資料長度即可。(但在你真正編寫程式碼之前,請務必閱讀"長度和寬度"!)

位元組順序修改器

在前面的章節中,我們學習了如何使用 nNvV 封裝和解封裝具有大端序或小端序位元組順序的整數。雖然這很好,但它仍然相當有限,因為它排除了所有類型的有號整數以及 64 位元整數。例如,如果你想以與平台無關的方式解封裝一系列有號大端序 16 位元整數,則必須撰寫

my @data = unpack 's*', pack 'S*', unpack 'n*', $buf;

這很醜陋。從 Perl 5.9.2 開始,有一種更棒的方式來表達你對特定位元組順序的渴望:>< 修改器。> 是大端序修改器,而 < 是小端序修改器。使用它們,我們可以將上述程式碼改寫為

my @data = unpack 's>*', $buf;

如你所見,箭頭的「大端」接觸到 s,這是一個很好的方式來記住 > 是大端序修改器。對於 < 來說,顯然也是一樣的,其中「小端」接觸到程式碼。

如果你必須處理大端序或小端序 C 結構,你可能會發現這些修改器更有用。請務必閱讀"封裝和解封裝 C 結構"以了解更多相關資訊。

浮點數

對於封裝浮點數,您可以在封裝碼 fdFD 之間進行選擇。fd 封裝到(或從)單精度或雙精度表示,由您的系統提供。如果您的系統支援,D 可用於封裝和解封裝(long double)值,其解析度甚至高於 fd請注意,有不同的長雙精度格式。

F 封裝 NV,這是 Perl 內部使用的浮點數類型。

實數沒有網路表示法,因此如果您想跨電腦邊界傳送實數,您最好堅持使用文字表示法,可能使用十六進位浮點格式(避免十進位轉換損失),除非您絕對確定線路的另一端是什麼。對於更具冒險精神的人,您也可以對浮點數碼使用上一節中的位元組順序修改器。

特殊範本

位元串

位元是記憶體世界中的原子。存取個別位元可能必須作為最後的手段使用,或因為這是處理資料最方便的方式。位元串(解)封裝在包含一系列 01 字元的字串和每個包含一組 8 個位元的位元組序列之間進行轉換。這幾乎和聽起來一樣簡單,除了位元組的內容可以用兩種方式寫成位元串。讓我們看看帶註解的位元組

  7 6 5 4 3 2 1 0
+-----------------+
| 1 0 0 0 1 1 0 0 |
+-----------------+
 MSB           LSB

這又是一個吃雞蛋的問題:有些人認為作為位元串,這應該寫成「10001100」,即從最高有效位元開始,而另一些人則堅持「00110001」。嗯,Perl 沒有偏見,所以這就是為什麼我們有兩個位元串碼

$byte = pack( 'B8', '10001100' ); # start with MSB
$byte = pack( 'b8', '00110001' ); # start with LSB

無法封裝或解封位元欄位,只能封裝或解封整數位元組。pack 總是從下一個位元組邊界開始,並透過視需要加入零位元,將其「進位」至下一個 8 的倍數。(如果您確實需要位元欄位,perlfunc 中有 "vec"。或者,您可以在未封裝的位元串上使用 split、substr 和串接,在字元串層級實作位元欄位處理。)

為了說明位元串的解封,我們將分解一個簡單的狀態暫存器(「-」代表「保留」位元)

+-----------------+-----------------+
| S Z - A - P - C | - - - - O D I T |
+-----------------+-----------------+
 MSB           LSB MSB           LSB

使用解封範本 'b16' 可以將這兩個位元組轉換為字串。若要從位元串取得個別的位元值,我們使用「空」分隔符號模式的 split,將其剖析成個別的字元。來自「保留」位置的位元值只會指定給 undef,這是一個方便的表示法,表示「我不在乎它放在哪裡」。

($carry, undef, $parity, undef, $auxcarry, undef, $zero, $sign,
 $trace, $interrupt, $direction, $overflow) =
   split( //, unpack( 'b16', $status ) );

我們也可以使用解封範本 'b12',因為最後 4 個位元可以忽略。

Uuencoding

範本字母表中另一個格格不入的字母是 u,它會封裝一個「uuencoded 字串」。(「uu」是 Unix-to-Unix 的縮寫。)您可能永遠不需要這種編碼技術,它是由於舊式傳輸媒體的缺點而發明,這些媒體僅支援簡單的 ASCII 資料。基本的配方很簡單:取三個位元組,或 24 個位元。將它們分成 4 個六個一組,並在每個六個一組中加入一個空白 (0x20)。重複此動作,直到所有資料都混合完畢。將 4 個位元組的群組折疊成不超過 60 個字元的行,並在前面加上原始位元組計數(增加 0x20)和結尾的 "\n"。- 當您在選單上選擇封裝代碼 u 時,pack 主廚會為您準備這道料理,現點現做

my $uubuf = pack( 'u', $bindat );

u 後面的重複計數會設定要放入 uuencoded 行中的位元組數目,預設為 45 的最大值,但可以設定為三的某個(較小的)整數倍數。unpack 只會忽略重複計數。

做加法

更奇怪的範本代碼是 %<number>。首先,因為它用作其他範本代碼的前置詞。其次,因為它根本無法用於 pack,第三,在 unpack 中,它不會傳回由其前置的範本代碼定義的資料。相反地,它會提供一個 number 位元的整數,它是透過對資料值做加法計算出來的。對於數值解封代碼,這並非難事

my $buf = pack( 'iii', 100, 20, 3 );
print unpack( '%32i3', $buf ), "\n";  # prints 123

對於字串值,% 會傳回位元組值的總和,讓您省去使用 substrord 進行總和迴圈的麻煩

print unpack( '%32A*', "\x01\x10" ), "\n";  # prints 17

儘管 % 程式碼記載為傳回「檢查碼」,但不要相信這些值!即使套用於少數位元組,它們也不會保證顯著的漢明距離。

bB 搭配使用時,% 只會新增位元,這可以用來有效計算已設定位元

my $bitcount = unpack( '%32b*', $mask );

而偶校驗位元可以這樣判定

my $evenparity = unpack( '%1b*', $mask );

Unicode

Unicode 是一種字元集,可以表示世界上大多數語言中的大多數字元,提供超過一百萬個不同字元的空間。Unicode 3.1 指定 94,140 個字元:基本拉丁字元被指定為數字 0 - 127。拉丁文-1 補充字元用於數種歐洲語言,位於下一個範圍,最高到 255。在更多拉丁文延伸字元後,我們會找到使用非羅馬字母的語言的字元集,穿插著各種符號集,例如貨幣符號、Zapf Dingbats 或盲文。(您可能想拜訪 https://www.unicode.org/ 來看看其中一些字元集 - 我個人最喜歡的是泰盧固語和坎納達語。)

Unicode 字元集將字元與整數關聯在一起。以相等的位元組數量編碼這些數字會讓以拉丁字母編寫的文字儲存需求增加一倍以上。UTF-8 編碼會儲存最常見(從西方觀點來看)的字元在單一位元組中,同時將較不常見的字元編碼在三個或更多位元組中,來避免這種情況。

Perl 在內部使用 UTF-8 編碼大多數 Unicode 字串。

那麼這與 pack 有什麼關係?嗯,如果您想撰寫 Unicode 字串(在內部以 UTF-8 編碼),您可以使用範本程式碼 U 來執行此操作。舉例來說,我們來產生歐元貨幣符號(代碼號碼 0x20AC)

$UTF8{Euro} = pack( 'U', 0x20AC );
# Equivalent to: $UTF8{Euro} = "\x{20ac}";

檢查 $UTF8{Euro} 會顯示它包含 3 個位元組:「\xe2\x82\xac」。不過,它只包含 1 個字元,數字 0x20AC。可以透過 unpack 完成來回傳遞

$Unicode{Euro} = unpack( 'U', $UTF8{Euro} );

使用 U 範本碼解壓縮也適用於 UTF-8 編碼的位元組字串。

通常你會想要壓縮或解壓縮 UTF-8 字串

# pack and unpack the Hebrew alphabet
my $alefbet = pack( 'U*', 0x05d0..0x05ea );
my @hebrew = unpack( 'U*', $utf );

請注意:一般來說,你最好使用 Encode::decode('UTF-8', $utf) 將 UTF-8 編碼的位元組字串解碼成 Perl Unicode 字串,並使用 Encode::encode('UTF-8', $str) 將 Perl Unicode 字串編碼成 UTF-8 位元組。這些函式提供處理無效位元組序列的方法,而且通常具有更友善的介面。

另一種可攜式二進制編碼

已新增壓縮碼 w 以支援可攜式二進制資料編碼方案,其功能遠遠超出簡單的整數。(詳細資訊請參閱 https://github.com/mworks-project/mw_scarab/blob/master/Scarab-0.1.00d19/doc/binary-serialization.txt,Scarab 專案。)BER (Binary Encoded Representation) 壓縮的無符號整數儲存基底 128 位元,最高有效位元在前,位元數盡可能少。除了最後一個位元組之外,每個位元組的第 8 位元(最高位元)都設定為 1。BER 編碼沒有大小限制,但 Perl 不會走極端。

my $berbuf = pack( 'w*', 1, 128, 128+1, 128*128+127 );

$berbuf 的十六進制傾印,在適當的位置插入空格,顯示為 01 8100 8101 81807F。由於最後一個位元組總是小於 128,因此 unpack 知道在哪裡停止。

範本分組

在 Perl 5.8 之前,範本的重複必須透過範本字串的 x 乘法來完成。現在有一個更好的方法,因為我們可以使用壓縮碼 () 搭配重複計數。堆疊架構範例中的 unpack 範本可以簡單地寫成這樣

unpack( 'v2 (vXXCC)5 v5', $frame )

讓我們進一步探討這個功能。我們將從以下內容的等效項開始

join( '', map( substr( $_, 0, 1 ), @str ) )

它會傳回一個字串,其中包含每個字串的第一個字元。使用壓縮,我們可以寫成

pack( '(A)'.@str, @str )

或者,因為重複計數 * 表示「重複所需次數」,因此可以簡寫成

pack( '(A)*', @str )

(請注意,範本 A* 只會完整封裝 $str[0]。)

要將儲存在三元組(日、月、年)中的日期封裝到陣列 @dates 中,成為位元組、位元組、短整數的順序,我們可以寫下

$pd = pack( '(CCS)*', map( @$_, @dates ) );

要在字串(偶數長度)中交換字元對,可以採用多種技術。首先,讓我們使用 xX 向前和向後跳躍

$s = pack( '(A)*', unpack( '(xAXXAx)*', $s ) );

我們也可以使用 @ 跳到偏移量,其中 0 是我們在遇到最後一個 ( 時所在的位置

$s = pack( '(A)*', unpack( '(@1A @0A @2)*', $s ) );

最後,還有一種完全不同的方法,即解開大端序短整數,並以相反的位元組順序封裝它們

$s = pack( '(v)*', unpack( '(n)*', $s );

長度和寬度

字串長度

在上一節中,我們看到了一個網路訊息,它是透過將二進位訊息長度加上實際訊息來建構的。您會發現封裝長度後加上這麼多位元組的資料是一個經常使用的配方,因為如果資料中可能包含空位元組,則附加空位元組將無法運作。以下是一個同時使用這兩種技術的範例:在兩個以空位元組終止的字串(包含來源和目的地位址)之後,會在長度位元組後傳送簡訊(到行動電話)

my $msg = pack( 'Z*Z*CA*', $src, $dst, length( $sm ), $sm );

使用相同的範本可以解開此訊息

( $src, $dst, $len, $sm ) = unpack( 'Z*Z*CA*', $msg );

在遠處潛伏著一個微妙的陷阱:在封裝時,在簡訊(變數 $sm 中)之後新增另一個欄位是沒問題的,但這無法輕易地解開

# pack a message
my $msg = pack( 'Z*Z*CA*C', $src, $dst, length( $sm ), $sm, $prio );

# unpack fails - $prio remains undefined!
( $src, $dst, $len, $sm, $prio ) = unpack( 'Z*Z*CA*C', $msg );

封裝程式碼 A* 會吞噬所有剩餘的位元組,而 $prio 仍然未定義!在我們讓失望澆熄士氣之前:Perl 擁有王牌,也可以執行這個技巧,只是需要再深入一點。看看這個

# pack a message: ASCIIZ, ASCIIZ, length/string, byte
my $msg = pack( 'Z* Z* C/A* C', $src, $dst, $sm, $prio );

# unpack
( $src, $dst, $sm, $prio ) = unpack( 'Z* Z* C/A* C', $msg );

將兩個封裝程式碼與斜線(/)結合,會將它們與引數清單中的單一值關聯起來。在 pack 中,會取得引數的長度,並根據第一個程式碼封裝,而引數本身會在轉換為斜線後的範本程式碼之後新增。這讓我們省去了插入 length 呼叫的麻煩,但我們真正得分的是在 unpack 中:長度位元組的值標記了要從緩衝區取得的字串的結尾。由於這種組合只有在第二個封裝程式碼不是 a*A*Z* 時才有意義,因此 Perl 允許您這樣做。

/ 之前的封包碼可以是任何適合表示數字的東西:所有數字二進制封包碼,甚至是 A4Z* 等文字碼

# pack/unpack a string preceded by its length in ASCII
my $buf = pack( 'A4/A*', "Humpty-Dumpty" );
# unpack $buf: '13  Humpty-Dumpty'
my $txt = unpack( 'A4/A*', $buf );

/ 未在 5.6 之前的 Perl 中實作,因此如果您的程式碼需要在舊版 Perl 上執行,您需要 unpack( 'Z* Z* C') 來取得長度,然後使用它來建立新的解封包字串。例如

# pack a message: ASCIIZ, ASCIIZ, length, string, byte
# (5.005 compatible)
my $msg = pack( 'Z* Z* C A* C', $src, $dst, length $sm, $sm, $prio );

# unpack
( undef, undef, $len) = unpack( 'Z* Z* C', $msg );
($src, $dst, $sm, $prio) = unpack ( "Z* Z* x A$len C", $msg );

但第二個 unpack 正在加速進行。它沒有使用簡單的文字字串作為範本。因此我們也許應該介紹...

動態範本

到目前為止,我們已經看到文字用作範本。如果封包項目清單沒有固定長度,則需要建立範本的運算式(只要因為某些原因無法使用 ()*)。以下是一個範例:要以 C 程式可以方便解析的方式儲存命名字串值,我們建立一個名稱和以 null 終止的 ASCII 字串序列,名稱和值之間有 =,後面再接一個額外的分隔 null 位元組。以下是方法

my $env = pack( '(A*A*Z*)' . keys( %Env ) . 'C',
                map( { ( $_, '=', $Env{$_} ) } keys( %Env ) ), 0 );

讓我們逐一檢查這個位元組工廠的齒輪。有一個 map 呼叫,建立我們打算塞進 $env 緩衝區的項目:它會將 = 分隔符號和雜湊項目值新增到每個金鑰(在 $_ 中)。每個三元組都使用範本碼序列 A*A*Z* 封裝,該序列會根據金鑰數量重複。 (是的,這就是 keys 函式在標量內容中傳回的內容。)要取得最後一個 null 位元組,我們在 pack 清單的結尾新增一個 0,並使用 C 封裝。 (專心的讀者可能已經注意到我們可以省略 0。)

對於反向操作,我們必須在讓 unpack 將其拆解之前,先確定緩衝區中的項目數量

my $n = $env =~ tr/\0// - 1;
my %env = map( split( /=/, $_ ), unpack( "(Z*)$n", $env ) );

tr 會計算 null 位元組。 unpack 呼叫會傳回一個名稱值配對清單,每個配對都會在 map 區塊中拆解。

計算重複

我們可以不用在資料項目(或項目清單)的尾端儲存哨兵,而是在資料前面加上計數。我們再次封裝雜湊的鍵和值,在每個值前面加上一個無符號短整數長度計數,並在最前面儲存成對的數量

my $env = pack( 'S(S/A* S/A*)*', scalar keys( %Env ), %Env );

這簡化了反向操作,因為重複的數量可以用 / 程式碼解封裝

my %env = unpack( 'S/(S/A* S/A*)', $env );

請注意,這是少數幾個無法對 packunpack 使用相同範本的情況,因為 pack 無法為 () 群組確定重複計數。

Intel HEX

Intel HEX 是一種檔案格式,用於將二進位資料表示為文字檔,主要用於編寫各種晶片。(請參閱 https://en.wikipedia.org/wiki/.hex 以取得詳細說明,以及 https://en.wikipedia.org/wiki/SREC_(file_format) 以取得 Motorola S 記錄格式,可以使用相同的技術解開。)每一行都以冒號 (':') 開頭,後面接著一系列十六進位字元,指定位元組計數 n (8 位元)、位址 (16 位元,大端序)、記錄類型 (8 位元)、n 個資料位元組和檢查碼 (8 位元),計算為前一個位元組的二補數和的最低有效位元組。範例::0300300002337A1E

處理此類行的第一步是將十六進位資料轉換為二進位,以取得四個欄位,同時檢查檢查碼。這並不令人意外:我們將從一個簡單的 pack 呼叫開始,將所有內容轉換為二進位

my $binrec = pack( 'H*', substr( $hexrec, 1 ) );

產生的位元組序列最適合用於檢查檢查碼。不要使用 for 迴圈來新增這個字串的位元組的 ord 值,讓你的程式變慢 - unpack 程式碼 % 是用於計算所有位元組的 8 位元和的工具,這個和必須等於零

die unless unpack( "%8C*", $binrec ) == 0;

最後,讓我們取得那四個欄位。到目前為止,你對前三個欄位應該沒有任何問題 - 但我們如何使用第一個欄位中的資料位元組計數作為資料欄位的長度?這裡的程式碼 xX 可以提供協助,因為它們允許在字串中來回跳躍以進行解封裝。

my( $addr, $type, $data ) = unpack( "x n C X4 C x3 /a", $bin ); 

程式碼 x 會略過一個位元組,因為我們還不需要計數。程式碼 n 會處理 16 位元大端序整數位址,而 C 會解開記錄類型。由於在偏移量 4,也就是資料開始的地方,我們需要計數。X4 會讓我們回到原點,也就是偏移量 0 的位元組。現在我們會取得計數,然後縮放至偏移量 4,現在我們已經完全準備好提取確切的資料位元組數量,讓尾隨的檢查和位元組保持原樣。

封裝和解封裝 C 結構

在先前的章節中,我們已經看過如何封裝數字和字元串。如果沒有幾個小問題,我們就可以用簡潔的說明立即結束本節,說明 C 結構不包含任何其他內容,因此你已經知道所有內容。抱歉,沒有:請繼續閱讀。

如果你必須處理大量的 C 結構,而且不想手動破解所有範本字串,你可能會想要看看 CPAN 模組 Convert::Binary::C。它不僅可以直接剖析你的 C 來源,而且還內建支援本節稍後描述的所有奇奇怪怪的東西。

對齊陷阱

在考量速度與記憶體需求時,天平已傾向於執行速度。這影響了 C 編譯器配置結構記憶體的方式:在 16 位元或 32 位元運算元可以在記憶體中的位置之間,或從 CPU 暫存器傳送得更快的架構上,如果它對齊在偶數或四的倍數,甚至八的倍數位址,C 編譯器會透過在結構中填入額外的位元組,為您提供這個速度優勢。如果您沒有跨越 C 碼岸線,這不太可能造成任何困擾(儘管您在設計大型資料結構時應該注意,或者您希望您的程式碼可以在架構之間移植(您希望這樣做,不是嗎?))。

要了解這如何影響 packunpack,我們將比較這兩個 C 結構

typedef struct {
  char     c1;
  short    s;
  char     c2;
  long     l;
} gappy_t;

typedef struct {
  long     l;
  short    s;
  char     c1;
  char     c2;
} dense_t;

通常,C 編譯器會為 gappy_t 變數配置 12 個位元組,但 dense_t 只需要 8 個位元組。在進一步調查後,我們可以繪製記憶體對應,顯示額外的 4 個位元組隱藏在哪裡

0           +4          +8          +12
+--+--+--+--+--+--+--+--+--+--+--+--+
|c1|xx|  s  |c2|xx|xx|xx|     l     |    xx = fill byte
+--+--+--+--+--+--+--+--+--+--+--+--+
gappy_t

0           +4          +8
+--+--+--+--+--+--+--+--+
|     l     |  h  |c1|c2|
+--+--+--+--+--+--+--+--+
dense_t

這就是第一個怪癖發生的位置:packunpack 範本必須填入 x 碼才能取得那些額外的填補位元組。

自然會問:「Perl 為什麼不能補償這些間隙?」這需要一個答案。一個充分的理由是 C 編譯器可能提供(非 ANSI)延伸,允許對結構對齊方式進行各種精細控制,甚至在個別結構欄位層級。而且,如果這還不夠,還有一個稱為 union 的陰險東西,其中填補位元組的數量無法僅從下一個項目對齊中衍生。

好,讓我們面對現實。以下是一種透過插入範本碼 x 來正確對齊的方法,它不會從清單中取得對應的項目

my $gappy = pack( 'cxs cxxx l!', $c1, $s, $c2, $l );

請注意 l 之後的 !:我們要確保將長整數打包成 C 編譯器編譯的樣子。即使現在,它也只會在編譯器對齊上述項目的平台上執行。而且在某個地方,有人有一個平台無法執行。[可能是 Cray,其中 shortintlong 都是 8 個位元組。 :-)]

計算位元組和觀察冗長結構中的對齊方式一定很麻煩。難道沒有辦法讓我們可以用一個簡單的程式建立範本嗎?以下是一個能辦到這件事的 C 程式

#include <stdio.h>
#include <stddef.h>

typedef struct {
  char     fc1;
  short    fs;
  char     fc2;
  long     fl;
} gappy_t;

#define Pt(struct,field,tchar) \
  printf( "@%d%s ", offsetof(struct,field), # tchar );

int main() {
  Pt( gappy_t, fc1, c  );
  Pt( gappy_t, fs,  s! );
  Pt( gappy_t, fc2, c  );
  Pt( gappy_t, fl,  l! );
  printf( "\n" );
}

輸出列可以用在 packunpack 呼叫中的範本

my $gappy = pack( '@0c @2s! @4c @8l!', $c1, $s, $c2, $l );

天啊,又是一個範本程式碼 - 好像我們還不夠多似的。但 @ 可以讓我們指定從封包緩衝區的開頭到下一個項目的位移,進而解決我們的問題:這正是 offsetof 巨集(定義在 <stddef.h>)在給定一個 struct 類型和其中一個欄位名稱(C 標準中的「成員標示符」)時傳回的值。

不使用位移或新增 x 來填補間隙都不令人滿意。(只要想像一下如果結構變更會發生什麼事。)我們真正需要的是一種方法來說「跳過到下一個 N 的倍數所需的位元組數」。在流暢的範本中,你可以用 x!N 來表示,其中 N 會替換成適當的值。以下是我們結構封裝的下一版本

my $gappy = pack( 'c x!2 s c x!4 l!', $c1, $s, $c2, $l );

這當然比較好,但我們仍然必須知道所有整數的長度,而且可攜性還差得很遠。例如,我們不想說 2,而是想說「一個 short 的長度」。但這可以用括號將適當的封包程式碼括起來來完成:[s]。因此,以下是我們能做到的最好版本

my $gappy = pack( 'c x![s] s c x![l!] l!', $c1, $s, $c2, $l );

處理位元組順序

現在,想像我們想要封裝資料給一個位元組順序不同的機器。首先,我們必須找出目標機器上資料類型的實際大小。假設 long 的寬度為 32 位元,而 short 的寬度為 16 位元。然後你可以將範本改寫成

my $gappy = pack( 'c x![s] s c x![l] l', $c1, $s, $c2, $l );

如果目標機器是小端序,我們可以寫成

my $gappy = pack( 'c x![s] s< c x![l] l<', $c1, $s, $c2, $l );

這會強制短成員和長成員採用小端序,如果您沒有太多結構成員,這很好。但我們也可以對群組使用位元組順序修改器,並撰寫下列程式碼

my $gappy = pack( '( c x![s] s c x![l] l )<', $c1, $s, $c2, $l );

這不如之前簡短,但它更明顯地表示我們打算為整個群組採用小端序位元組順序,而不仅仅是個別範本代碼。它也可以更具可讀性和更容易維護。

對齊,第 2 回合

我擔心我們還沒完全解決對齊問題。當您封裝結構陣列時,九頭蛇會再冒出另一個醜陋的頭

typedef struct {
  short    count;
  char     glyph;
} cell_t;

typedef cell_t buffer_t[BUFLEN];

問題在哪裡?在第一個欄位 count 之前和此欄位與下一個欄位 glyph 之間都不需要填補,那麼為什麼我們不能像這樣封裝

# something goes wrong here:
pack( 's!a' x @buffer,
      map{ ( $_->{count}, $_->{glyph} ) } @buffer );

這會封裝 3*@buffer 位元組,但結果發現 buffer_t 的大小是 BUFLEN 的四倍!這個故事的寓意是,結構或陣列所需的對齊會傳播到下一個更高級別,在該級別中我們必須考慮每個組件結尾的填補。因此,正確的範本是

pack( 's!ax' x @buffer,
      map{ ( $_->{count}, $_->{glyph} ) } @buffer );

對齊,第 3 回合

即使您考慮了以上所有事項,ANSI 仍然允許這樣做

typedef struct {
  char     foo[2];
} foo_t;

大小有所不同。結構的對齊限制可能大於其任何元素。[如果您認為這不會影響任何常見事項,請肢解您看到的下一個手機。許多手機都有 ARM 核心,而 ARM 結構規則會讓 sizeof (foo_t) == 4]

如何使用指標

此節標題指出您在封裝 C 結構時可能會遇到的第二個問題。如果您打算呼叫的函式預期 void * 值,您不能僅取用 Perl 變數的參考。(雖然該值確實是記憶體位址,但它並非變數內容儲存的位址。)

範本代碼 P 承諾封裝「固定長度字串的指標」。這不是我們想要的嗎?讓我們試試看

# allocate some storage and pack a pointer to it
my $memory = "\x00" x $size;
my $memptr = pack( 'P', $memory );

但等一下:pack 不僅僅回傳一個位元組序列嗎?我們如何將這個位元組字串傳遞給某個 C 程式碼,而這個程式碼預期一個指標,而指標畢竟只是一個數字?答案很簡單:我們必須從 pack 回傳的位元組取得數字位址。

my $ptr = unpack( 'L!', $memptr );

顯然這假設可以將指標類型轉換為無號長整數,反之亦然,這通常可行,但不能視為普遍定律。- 現在我們有了這個指標,接下來的問題是:我們如何善用它?我們需要呼叫某個 C 函式,其中預期一個指標。read(2) 系統呼叫浮現在腦海

ssize_t read(int fd, void *buf, size_t count);

在閱讀 perlfunc 說明如何使用 syscall 之後,我們可以撰寫這個 Perl 函式,將檔案複製到標準輸出

require 'syscall.ph'; # run h2ph to generate this file
sub cat($){
    my $path = shift();
    my $size = -s $path;
    my $memory = "\x00" x $size;  # allocate some memory
    my $ptr = unpack( 'L', pack( 'P', $memory ) );
    open( F, $path ) || die( "$path: cannot open ($!)\n" );
    my $fd = fileno(F);
    my $res = syscall( &SYS_read, fileno(F), $ptr, $size );
    print $memory;
    close( F );
}

這既不是簡潔的範例,也不是可移植性的典範,但它說明了重點:我們能夠潛入幕後,存取 Perl 否則受到嚴密保護的記憶體!(重要注意事項:Perl 的 syscall 不需要 你以這種迂迴的方式建構指標。你只需傳遞一個字串變數,Perl 就會轉發位址。)

unpack 如何與 P 一起使用?想像緩衝區中即將解壓縮的一些指標:如果它不是空指標(它會明智地產生 undef 值),我們有一個起始位址 - 但接下來呢?Perl 無法得知這個「固定長度字串」有多長,因此由你指定 P 之後實際大小作為明確長度。

my $mem = "abcdefghijklmn";
print unpack( 'P5', pack( 'P', $mem ) ); # prints "abcde"

因此,pack 會忽略 P 之後的任何數字或 *

現在我們已經看到 P 的運作方式,我們不妨也讓 p 旋轉一下。我們為什麼需要一個第二個範本程式碼來封裝指標?答案就在於一個簡單的事實:使用 punpack 承諾一個從緩衝區取得的位址開始的以 null 結束的字串,這表示要回傳的資料項目的長度

my $buf = pack( 'p', "abc\x00efhijklmn" );
print unpack( 'p', $buf );    # prints "abc"

儘管這可能會令人困惑:由於長度是由字串長度暗示的,因此在封包代碼 p 之後的數字是重複次數,而不是像在 P 之後那樣的長度。

使用 pack(..., $x) 搭配 Pp 來取得實際儲存 $x 的位址時,必須謹慎使用。Perl 的內部機制將變數和該位址之間的關係視為其自身的私密事項,並不真正關心我們已取得一份副本。因此

不過,P 或 p 封裝字串文字是安全的,因為 Perl 僅會配置一個匿名變數。

封裝食譜

以下是 packunpack 的一些(可能)有用的罐頭食譜

# Convert IP address for socket functions
pack( "C4", split /\./, "123.4.5.6" ); 

# Count the bits in a chunk of memory (e.g. a select vector)
unpack( '%32b*', $mask );

# Determine the endianness of your system
$is_little_endian = unpack( 'c', pack( 's', 1 ) );
$is_big_endian = unpack( 'xc', pack( 's', 1 ) );

# Determine the number of bits in a native integer
$bits = unpack( '%32I!', ~0 );

# Prepare argument for the nanosleep system call
my $timespec = pack( 'L!L!', $secs, $nanosecs );

對於一個簡單的記憶體傾印,我們將一些位元組解封包成相同數量的十六進位數字對,並使用 map 來處理傳統間距 - 每行 16 個位元組

my $i;
print map( ++$i % 16 ? "$_ " : "$_\n",
           unpack( 'H2' x length( $mem ), $mem ) ),
      length( $mem ) % 16 ? "\n" : '';

趣聞部分

# Pulling digits out of nowhere...
print unpack( 'C', pack( 'x' ) ),
      unpack( '%B*', pack( 'A' ) ),
      unpack( 'H', pack( 'A' ) ),
      unpack( 'A', unpack( 'C', pack( 'A' ) ) ), "\n";

# One for the road ;-)
my $advice = pack( 'all u can in a van' );

作者

Simon Cozens 和 Wolfgang Laun。