内容

名称

perlpacktut - 关于 packunpack 的教程

描述

packunpack 是两个函数,用于根据用户定义的模板转换数据,在 Perl 存储值的保护方式和 Perl 程序环境中可能需要的某种明确表示之间进行转换。不幸的是,它们也是 Perl 提供的两个最容易被误解和最常被忽视的函数。本教程将为您揭开它们的奥秘。

基本原理

大多数编程语言不会保护存储变量的内存。例如,在 C 中,您可以获取某个变量的地址,sizeof 运算符会告诉您分配给该变量的字节数。使用地址和大小,您可以随意访问存储空间。

在 Perl 中,你无法随机访问内存,但 `pack` 和 `unpack` 提供的结构和表示转换是一个极好的替代方案。`pack` 函数将值转换为包含根据给定规范的表示的字节序列,即所谓的“模板”参数。`unpack` 是反向过程,从字节字符串的内容中推导出一些值。(但请注意,并非所有打包在一起的东西都能整齐地解包 - 这是经验丰富的旅行者可能会证实的常见体验。)

你可能会问,为什么你需要一个包含一些值的二进制表示的内存块?一个很好的理由是输入和输出访问某个文件、设备或网络连接,在这种情况下,这种二进制表示要么是强制的,要么会在处理中给你带来一些好处。另一个原因是将数据传递给一些系统调用,这些系统调用在 Perl 函数中不可用:`syscall` 要求你提供以 C 程序中发生的方式存储的参数。即使文本处理(如下一节所示)也可以通过明智地使用这两个函数来简化。

为了了解 (un)packing 的工作原理,我们将从一个简单的模板代码开始,其中转换处于低速档:字节序列的内容和十六进制数字字符串之间。让我们使用 `unpack`,因为这可能会让你想起一个转储程序,或者一些不幸的程序在它们消亡到广阔的蓝天中之前,会向你抛出一些绝望的最后信息。假设变量 `$mem` 保存一个我们想要检查的字节序列,而无需假设它的含义,我们可以写

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

然后我们可能会看到类似这样的东西,其中每对十六进制数字对应一个字节

41204d414e204120504c414e20412043414e414c2050414e414d41

这个内存块里有什么?数字、字符,还是两者的混合?假设我们使用的是 ASCII(或类似的)编码的计算机:十六进制值在 `0x40` - `0x5A` 范围内表示大写字母,`0x20` 表示空格。因此,我们可以假设它是一段文本,有些人可以像读小报一样阅读它;但其他人则必须找到一个 ASCII 表,并重温一下那个一年级的感觉。我们不太关心如何阅读它,我们注意到 `unpack` 使用模板代码 `H` 将字节序列的内容转换为常用的十六进制表示法。由于“一系列”对于数量来说是一个相当模糊的指示,因此 `H` 被定义为仅转换单个十六进制数字,除非它后面跟着一个重复计数。星号表示重复计数,表示使用剩余的所有内容。

反向操作 - 从十六进制数字字符串中打包字节内容 - 也同样容易编写。例如

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

由于我们向 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 列 - 10 个字符宽。pack 中的“字符”表示为 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.

Ugh。难怪。零字节(字符 0)和空格(字符 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 结构" 以了解更多信息。

浮点数

对于打包浮点数,您可以选择打包代码 fdFDfd 按您的系统提供的单精度或双精度表示进行打包(或解包)。如果您的系统支持,D 可用于打包和解包(long double)值,这可以提供比 fd 更高的分辨率。请注意,存在不同的 long double 格式。

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"。或者您可以在字符字符串级别实现位字段处理,使用 splitsubstrconcat 对解包的位字符串进行操作。)

为了说明位字符串的解包,我们将分解一个简单的状态寄存器(“-” 代表“保留”位)

+-----------------+-----------------+
| 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,但可以设置为 3 的某个(较小)整数倍。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(二进制编码表示)压缩无符号整数存储以 128 为基的数字,最高有效数字在前,并且尽可能少地使用数字。除了最后一个字节之外,每个字节的第 8 位(最高位)都被设置。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,我们可以写

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

长度和宽度

字符串长度

在上一节中,我们看到了一个网络消息,该消息是通过在实际消息之前添加二进制消息长度来构建的。你会发现,打包长度后紧跟着这么多字节的数据是一个常用的方法,因为如果空字节可能是数据的一部分,则追加空字节将不起作用。以下是一个同时使用这两种技术的示例:在两个以 null 结尾的字符串(包含源地址和目标地址)之后,在长度字节之后发送短消息(到手机)。

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,有关 Motorola S 记录格式,请参见 https://en.wikipedia.org/wiki/SREC_(file_format),可以使用相同的技术对其进行解开。)每行以冒号(':')开头,后面跟着一系列十六进制字符,指定字节计数 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 寄存器到 CPU 寄存器之间更快移动的架构上,如果它对齐到偶数或 4 的倍数,甚至对齐到 8 的倍数的地址,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 );

哇,又一个模板代码——好像我们还没有很多。但是 @ 通过让我们能够指定从打包缓冲区开头到下一个项目的偏移量来拯救了我们的一天:这正是当给定一个 struct 类型及其字段名称之一(在 C 标准中称为“成员指定符”)时,offsetof 宏(在 <stddef.h> 中定义)返回的值。

使用偏移量或添加 x 来弥合间隙都不令人满意。(想象一下,如果结构发生变化会发生什么。)我们真正需要的是一种方法来表示“跳过到下一个 N 的倍数所需字节数”。在流畅的模板中,你可以用 x!N 来表示,其中 N 被替换为适当的值。以下是我们的结构打包的下一个版本。

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

这当然更好,但我们仍然需要知道所有整数的长度,而且可移植性还很遥远。例如,我们不想使用 `2`,而是想说“一个短整数的长度”。但这可以通过将适当的打包代码括在方括号中来实现:`[s]`。因此,这是我们能做的最好的

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

处理字节序

现在,假设我们想为具有不同字节序的机器打包数据。首先,我们需要确定目标机器上数据类型的实际大小。假设长整数为 32 位宽,短整数为 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 );

这不像以前那样简短,但它更清楚地表明我们打算对整个组使用小端序字节序,而不仅仅是对单个模板代码使用。它也更易读,更容易维护。

对齐,第二部分

我担心我们还没有完全解决对齐问题。当您打包结构数组时,这个多头怪兽又露出了丑陋的脑袋

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

typedef cell_t buffer_t[BUFLEN];

问题出在哪里?在第一个字段 `count` 之前不需要填充,在 `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 );

对齐,第三部分

即使您考虑了以上所有因素,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);

在阅读了解释如何使用syscallperlfunc之后,我们可以编写这个将文件复制到标准输出的 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 就会转发地址。)

unpackP 如何工作?想象一下要解包的缓冲区中的某个指针:如果它不是空指针(它会巧妙地产生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"

虽然这可能令人困惑:由于字符串的长度是由字符串的长度隐含的,因此在 pack 代码 `p` 之后的一个数字是重复次数,而不是像 `P` 之后那样是长度。

使用 `pack(..., $x)` 与 `P` 或 `p` 来获取 `$x` 实际存储的地址必须谨慎使用。Perl 的内部机制将变量与其地址之间的关系视为其自身的私有事务,并不关心我们是否获得了副本。因此

但是,对字符串字面量进行 P 或 p 打包是安全的,因为 Perl 只会分配一个匿名变量。

打包食谱

这里收集了一些(可能)有用的 `pack` 和 `unpack` 预制食谱

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