sz/rz实现及cat binary文件时乱码问题

一、嵌入式系统中文件传输
这个工具之前还的确是没有使用到过,可能的原因是因为之前一直使用桌面系统fedora core发行版本,开发主要使用busybox文件系统,而这两种版本中都没有自带sz/rz工具。它们的作用是通过串口来发送和接收文件,虽然说是串口,所有的支持串口协议的软件或者链路都可以,例如使用telnet/ssh之类的远程链接工具,主机之间的通讯使用网卡来实现,而服务器端的主机通过伪终端来模拟一个串口链路,但是这两者结合就可以方便的进行文件传递。
这里工具的使用也就不多说了,因为大部分情况下大家使用也就是一个sz file 或者rz两个命令,而这两种格式是可以满足我们绝大部分的需求的。但是这个工具很有意思的是它无论中间经过了多少个telnet的中转,最后它都可以直达最为原始的接入端,也就是我们最为常见的windows下使用的secureCRT客户端,这一点在网络泛滥的今天乍一看还是有些神奇的。为什么一个请求可以翻山越岭、跋山涉水的直接到达连接发起的客户端而没有在中间的任何一种中转停留,一个报文是如何知道自己发向合入?当执行rz/sz的时候,这两个任务是不知道自己即将连接的客户端在哪里。
二、实现原理
事实上sz/rz使用的并不是我们常见的网络协议,而是使用了最为原始的基于串口的字节流协议。关于这一点,我在之前的一些日志中大致说明了一下在终端中经常使用的ESC序列,它们常见的功能就是控制终端中文字的显示颜色功能,例如通过ls默认显示压缩文件为红色的功能就是基于这种协议实现的。大致的说,就是我们常见的字节编码就是ASCII编码,这个编码中前32个字符是控制字符,它们在终端中是不会被显示的,而是用来控制显示。和冯诺依曼编码相同,它直接复用了链路中的字节流编码,同一个链路中,同样的字节单位,有的是代码,有的是数据。这些控制字符随着GUI的横行而越来越少的被大家使用和关注,所以可能很多人甚至都没有注意过ASCII码中前32个字节的存在。
这种风格对大家入学就学习《计算机网络》的同学来说可能有些不可思议,因为这是一种比较强的耦合关系,需要对同一个输出中的数据做不同的解释,看来总是有些简陋和诡异,但是这就是当时计算机的现实,这些传统不管在现在看来多么不可思议,它依然还是在影响着我们现在的计算机规范,一如那些历史课本中那些看似尘封的往事依然在影响着我们现在的生活一样。
关于字符集的编码总结,在赵炯《Linux内核完全剖析(第一版)》第10.4.3.3一节中有比较详细的描述,对于理解这个内容应该是很好的资料,当然最好的资料就是相关实现的源代码,不过代码这种东西很多人没有时间看,另外还有网络上一个比较好的解释,只是一个问题的恢复中的,贴出地址为https://bbs.archlinux.org/viewtopic.php?pid=423358#p423358。
这里串口通讯的基础假设是确定的:串口上所有的数据都是按照控制字符和可显字符严格确认,并且假设串口的所有使用者都明白并遵守这个假设。我们回头看一下cat显示二进制文件时候的情况,事实上一个cat程序并不知道自己显示的字节流是什么内容,所以说它是不遵守这个假设的,不遵守的结果就是造成了不可预料的行为,最为直观的现象就是显示的乱码问题。
三、串口乱码问题由来
对于控制字符来说,最为原始的32字符是不够用的,因为一个中断上颜色显示、光标移动、翻页等各种参数设置和查询需要很多的指令,此时就需要控制字符可以扩展。这里描述的所有扩展字符控制都是通过esc开始(ascii码为0x1b,八进制033),这其实相当于一个编码树,根节点为esc,然后生长出诸多的节点表示不同的序列和意义。
对于我们的乱码显示问题,这里涉及到字符集的选择问题。对于同样的内码,它虽然是可显示内容,但是它具体显示为什么内容却可以有不同的解释。对于字符集的装载是通过SCS指令序列(Select Character Set)来设定,这里的序列为 esc ( Ps和 Esc } Ps,其中前者用来选择G0(通俗的说就是内码小于128的显示字符),后者选择G1(内码大于128小于256的显示字符),而Ps的可选值有5个,分别为 A B 0 1 2分别表示UK字符集,US字符集,0 图形字符集,1另选ROM字符集、2另选ROM特殊字符集。任意时刻,一个终端可以有两个活动字符集,并且可以通过标准单字节控制字SI(Shift In)和SO(Shift Out)进行切入和切出。
对于上面的说明,可以看一下内核的实现,同样是在vt.c中,这个是对伪终端的一种模拟而不是真正的终端,所以它的实现和这里描述的有些不同,具体更加详细的原因和文档就不得而知了。
linux-2.6.21\drivers\char\vt.c
static void do_con_trol(struct tty_struct *tty, struct vc_data *vc, int c)
    case 14://(shift out)
        vc->vc_charset = 1;
        vc->vc_translate = set_translate(vc->vc_G1_charset, vc);
        vc->vc_disp_ctrl = 1;
        return;
    case 15://(shift in)
        vc->vc_charset = 0;
        vc->vc_translate = set_translate(vc->vc_G0_charset, vc);
        vc->vc_disp_ctrl = 0;
        return;

case ESsetG0:
        if (c == '0')
            vc->vc_G0_charset = GRAF_MAP;
        else if (c == 'B')
            vc->vc_G0_charset = LAT1_MAP;
        else if (c == 'U')
            vc->vc_G0_charset = IBMPC_MAP;
        else if (c == 'K')
            vc->vc_G0_charset = USER_MAP;
        if (vc->vc_charset == 0)
            vc->vc_translate = set_translate(vc->vc_G0_charset, vc);
        vc->vc_state = ESnormal;
        return;
    case ESsetG1:
        if (c == '0')
            vc->vc_G1_charset = GRAF_MAP;
        else if (c == 'B')
            vc->vc_G1_charset = LAT1_MAP;
        else if (c == 'U')
            vc->vc_G1_charset = IBMPC_MAP;
        else if (c == 'K')
            vc->vc_G1_charset = USER_MAP;
        if (vc->vc_charset == 1)
            vc->vc_translate = set_translate(vc->vc_G1_charset, vc);
        vc->vc_state = ESnormal;
        return;
其中set_translate使用了四张表,根据不同的参数来使用不同的映射方法。如果cat 一个二进制文件的时候,这个二进制文件很可能会包含这种修改系统字符集的操作,而修改之后显示的现象就是乱码。这里恢复有两种方式一种是使用用户态的reset命令完成恢复,另一种时使用终端内置的 esc c序列来完成恢复,内核代码为
    switch(vc->vc_state) {
    case ESesc:
……
        case 'c':
            reset_terminal(vc, 1);
            return;
我们这里可以测试一下,由于乱码无法复制出来,这里抓个图,可以看到在执行了单字符的SI之后,已经开始出现乱码,最后盲打执行 echo -e '\016'也即 SO之后恢复正常。这个SO只是一个开关,或者说toggle功能,更为复杂的混论需要通过esc c序列来恢复,这个序列应该为 echo -e '\033\0143'

sz/rz实现及cat binary文件时乱码问题 - Tsecer - Tsecer的回音岛

 
四、sz/rz协议实现
即使在没有使用网络之前,主机之间的协议已经出现,并且同样是以数据帧(frame)为单位进行交互。前一节说明了字符显示的常用控制方法,那么对于sz/rz来说,它同样也引入了自己的转义序列,但是它并没有使用esc字符引导,而是使用了另一个控制字符CAN(030)来标志一个sz/rz帧的开始,之后再定义自己的序列。
这里可以回答开始说的sz/rz在发送时如何知道经过多个中转telnet之后最终落脚点的。sz/rz使用的是串口协议而不是网口协议,也就是这个协议命令和普通字符流一样是作为数据通过telnet的socket传递到客户端,telnet在这个过程中对这个控制字符一无所知,因为它只是作为一个通讯工具来传输这个报文而不会解析报文中的内容。如果非要用流行的网络协议来说明的话,那么这个协议是在OSI协议的应用层(telnet之上),telnet程序不解释自己传递的数据而只是负责传递。之后真正的检测套接口中数据字节流的程序才可以感知到这个协议。
当使能secureCRT的sz/rz模块的时候,secureCRT不仅会完成基本的telnet指令,它同样还会对telnet中的数据流进行监控,当它检测到一个zmodem模式的报文的时候,它就进行相应的响应。反之,如果说客户端没有使能这个检测的话,那么它就对这个协议视而不见。
可以看到,这个协议还是比较简陋的,如果你不小心在secureCRT中执行了二进制文件的显示命令并且使能了客户端的zmodem的话,可能如果突然间弹出了一个文件下载窗口或者不小心从串口中读到了一些奇怪的二进制数据,那也是有可能的。
五、关于sz/rz的一些资料
http://www.ifremer.fr/lpo/gliders/donnees_tt/tech/protocoles/

posted on 2019-03-07 09:06  tsecer  阅读(1356)  评论(0编辑  收藏  举报

导航