解析 7 bit PDU 时,为什么 '@' 被解析为 0x00?
0x00 是正确的解包结果。在 GSM 7-bit 默认字母表中,@ 的码值是 0x00;在 ASCII 中,它的码值是 0x40。
当前代码已经完成 septet 解包,接下来还需要把 GSM 字符码转换为输出使用的字符编码。原代码的长度设置和数组读取也需要修正。
为什么 @ 会得到 0x00
SMS 使用的 GSM 7-bit 默认字母表与 ASCII 不同。每个字符占 7 bit,并不代表它使用 ASCII 编码。
例如:
| 字符 | GSM 7-bit 码值 | ASCII 码值 |
|---|---|---|
@ | 0x00 | 0x40 |
p | 0x70 | 0x70 |
= | 0x3D | 0x3D |
题中多数字符在两种编码中的码值恰好一致,直接输出解包结果也能正常显示,遇到 @ 时才出现差别。
完整的处理过程是:
打包后的 TP-UD 字节
→ 解包为 7-bit septet
→ 按 GSM 字符表转换为字符
→ 编码为 UTF-8 等输出格式
第二步得到 0x00,之后按 GSM 字符表将它映射为 @,最终输出的 ASCII 或 UTF-8 字节是 0x40。在线解析器显示 @,也是经过了字符转换,不能据此认为解包时就应该得到 0x40。
@ 对应的位在哪里
从零开始计数,@ 是正文中索引为 15 的字符。它在打包数据中的起始位置是:
bit_pos = 15 × 7 = 105
byte_idx = 105 / 8 = 13
bit_idx = 105 % 8 = 1
它占据 uArr[13] 的 bit 1 到 bit 7。这个字节是 0x00,取出这七位得到的也是 0x00。
0x40 的二进制表示是 1000000,其中的 1 位于 bit 6,在七位范围内,ch & 0x7F 会保留这一位。当前得到零,是因为 GSM 字符表把 @ 编码为零。
题中的 21 个打包字节解包后,应得到以下 24 个 septet:
70 77 3D 31 32 33 34 35 36 71 51 2C
70 73 3D 00 26 3B 3F 21 2D 2F 3A 3B
其中,3D 00 26 对应 =@&。
先修正长度和边界读取
完整 PDU 中的相关字段为:
| 字段 | 值 | 含义 |
|---|---|---|
| TP-DCS | 0x00 | 使用 GSM 7-bit 默认字母表 |
| TP-UDL | 0x18 | 用户数据长度为 24 个 septet |
| TP-UDHI | 0 | 没有用户数据头 UDH |
循环次数应为 0x18,即 24 次。原代码设置的 0x19 是 25,会多解包一个字符。
24 个 septet 占用 21 个字节,与 uArr 的实际长度一致:
ceil(24 × 7 / 8) = 21
原代码还会在下面这段逻辑中越界读取:
if (bit_idx > 0) {
ch |= (uint16_t)uArr[byte_idx + 1] << (8 - bit_idx);
}
当 bit_idx == 1 时,七位数据都在当前字节内,无须读取下一个字节。最后一个字符正好属于这种情况,再访问 uArr[byte_idx + 1] 就会越界。条件应改为:
if (bit_idx > 1)
针对这条短信的修正示例
题中这条短信除 @ 外,其余字符的 GSM 码值都与 ASCII 一致,因此解包后只需额外转换 0x00:
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
int main(void)
{
const uint8_t uArr[] = {
0xF0, 0x7B, 0x2F, 0x26, 0x9B, 0xD1, 0x6A,
0xB6, 0x78, 0x94, 0x05, 0x9F, 0xF7, 0x00,
0xA6, 0xDD, 0x2F, 0xD4, 0x7A, 0xE9, 0x76
};
/* 来自这条 PDU 的 TP-UDL:0x18。 */
enum { SEPTET_COUNT = 24 };
char out[SEPTET_COUNT + 1];
size_t required_bytes = (SEPTET_COUNT * 7u + 7u) / 8u;
if (required_bytes > sizeof uArr) {
fputs("Truncated user data\n", stderr);
return 1;
}
for (size_t i = 0; i < SEPTET_COUNT; ++i) {
size_t bit_pos = i * 7u;
size_t byte_idx = bit_pos / 8u;
unsigned bit_idx = (unsigned)(bit_pos % 8u);
uint16_t ch = (uint16_t)(uArr[byte_idx] >> bit_idx);
if (bit_idx > 1u) {
ch |= (uint16_t)uArr[byte_idx + 1u]
<< (8u - bit_idx);
}
uint8_t septet = (uint8_t)(ch & 0x7Fu);
/*
* 仅覆盖题中短信使用的字符。
* 通用解析器应在这里执行完整的 GSM 字符映射。
*/
out[i] = (septet == 0x00u) ? '@' : (char)septet;
}
out[SEPTET_COUNT] = '\0';
printf("%s\n", out);
return 0;
}
对应的文本为:
pw=123456qQ,ps=@&;?!-/:;
通用解析器还要注意什么
将 0x00 替换为 @ 只适用于上面的示例。默认字母表中还有其他与 ASCII 不同的码值,例如 GSM 0x40 表示 ¡。遇到 0x1B 时,需要结合后一个 septet 按扩展字符表解码。通用解析器应完成完整的字符映射,再生成 UTF-8 等输出字节。
解包得到的 septet 数组要用显式长度保存。数组中的 0x00 是有效的 GSM 字符码,C 字符串函数却会把它当作结束标志,因此不能直接将该数组当作 C 字符串使用。应在完成字符转换后再添加字符串终止符。
输出时应使用:
printf("%s\n", out);
直接写 printf(out) 会把短信内容当作格式字符串。短信中一旦出现 %,就可能触发错误的格式解析。
上述位偏移计算适用于这条没有 UDH 的消息。解析带 UDH 的 GSM 7-bit 短信时,还需要处理头部长度及填充位,正文起点不能直接套用这里的零偏移。
备注:内容仅供参考。
