Linux那些事儿 之 戏说USB(37)字符串描述符
关于字符串描述符,前面的前面已经简单描述过了,但是因为现在长夜漫漫,孤枕难眠,所以多说点。字符串描述符的地位仅次于设备/配置/接口/端点四大描述符,那四大设备必须得支持,而字符串描述符对设备来说则是可选的,类似于网球里马德里大师赛与四大满贯之间的地位差异,四大满贯是个碗儿都争着抢着爬着也要去,而号称第五大满贯的马德里大师赛却动不动就会被大碗儿们因各种理由给涮了。
这并不是就说字符串描述符不重要,对咱们来说,字符串要比数字亲切的多,提供字符串描述符的设备也要比没有提供的设备亲切的多,不会有人会专门去记前面使用lsusb列出的04b4表示的是Cypress Semiconductor Corp.,如果谁真费心去记了,俺也不会pfpf,而是只会发出和看到06年那几个超女时一样的感叹:动物的种类在减少,人的种类在增加吗?
一提到字符串,不可避免就得提到字符串使用的语言。字符串亲切是亲切,但不像数字那样全球通用,使用中文了,老外看不懂,使用法文阿拉伯文什么的咱又看不懂,你知道目前世界上有多少种语言吗?有得说七千多种,有得说五千多种,无一定论,不过使用人口超过100万的语言也足足有140多种。字符串描述符也需要应对多种语言的情况,当然这并不是说设备里就要存储这么多不同语言的字符串描述符,这未免要求过高了些,代价也忒昂贵了些,要知道这些描述符不会凭空生出来,是要存储在设备的EEPROM里的,此物是需要MONEY的,现在都在提倡节约型社会,要节约MONEY,尽量少占用EEPROM,要节约用水,尽量和女友一起洗澡。所以说只提供几种语言的字符串描述符就可以了,甚至说只提供一种语言,比如英语就可以了。
其实咱们现在说的语言的问题和秦始皇统一六国时遇到的语言问题一样,就是太多了,鸡说鸡的,鸭说鸭的,交流成问题。你说啥时候地球上或整个宇宙上只说一种语言了,咱也不用费劲去学什么英语了,就是外星人来了,大家和外星mm交流也不成问题。不过不管哪种语言,在PC里或者设备里存放都只能用二进制数字,这就需要在语言与数字之间进行编码,这个所谓的编码和这个世界上其它事物一样,都是有多种的,你说连人的种类都有很多了,人造出来的编码种类能没有很多么?起码每种语言都会存在独立的编码方式,咱们的简体中文可以使用GB2312、GBK、GB18030等,台湾那边儿是繁体,用的就是big5,这么一来每种语言自己内部交流是不成问题了,可相互之间就像鸡同鸭讲了。于是世界上的某些角落就出现了那么一些有志青年,立志要将各种语言的编码体系给统一起来,于是就出现了UNICODE和ISO-10646。比起他们,俺是太没追求了,父亲问我人生有什么追求?我回答金钱和美女,父亲凶狠的打了我的脸,我回答事业与爱情,父亲赞赏的摸了我的头。
Spec里就说了,字符串描述符使用的就是UNICODE编码,usb设备里的字符串可以通过它来支持多种语言,不过你需要在向设备请求字符串描述符的时候指定一个你期望看到的一种语言,俗称语言ID,即Language ID。这个语言ID使用两个字节表示,所有可以使用的语言ID在http://www.usb.org/developers/docs/USB_LANGIDs.pdf 文档里都有列出来,从这个文档里你也可以明白为啥要使用两个字节,而不是一个字节表示。这么说吧,比如中文是0X04,但是中文还有好几种,所以就需要另外一个字节表示是哪种中文,简体就是0X02,注意合起来表示简体中文并不是0X0402或者0X0204,因为这两个字节并不是分的那么清,bit0~9一共10位去表示Primary语言ID,其余6位去表示Sub语言ID,毕竟一种语言的Sub语言不可能会特别的多,没必要分去整整一半8bits,所以简体中文的语言ID就是0X0804。
不多罗唆,还是结合代码,从上节飘过的usb_cache_string说起,看看如何去获得一个字符串描述符,它在message.c里定义
每个成年人都有那么一个身份证号码,每个字符串描述符都有一个序号,身份证号可能会重复,字符串描述符这个序号是不能重复的,不过这点不用你我操心,都是设备已经固化好了的东西,重复不重复也不是咱们要操心的事。咱们要操心的事太多了,要操心吃还要操心睡,加菲猫告诉我们,除了吃和睡,生命或许还有别的意义,不过我觉得没有就挺好。
也好理解,什么东西一多了,最好最节约最省事的区分方式就是编号,字符串描述符当然可以有很多个,参数的index就是表示了你希望获得其中的第几个。但是不可疏忽大意的是,你不能指定index为0,0编号是有特殊用途的,你指定0了就什么也得不到。你去华为找工号000的,不会有人应你,根本就没这人,你找001,这次有人应你,不过是保安,赶你走的,没事儿找任老总干吗,没看一个接一个的自杀正是多事之秋么。
有关这个函数,还需要明白两点,第一是它采用的方针策略,就是苦活儿累活儿找个usb_string()去做,自己一边儿看着,加菲猫还告诉我们,工作好有意思耶!尤其是看着别人工作。这个usb_string()怎么工作的之后再看,现在只要注意下它的参数,比usb_cache_string()的参数多了两个,就是buf和size,也就是需要传递一个存放返回的字符串描述符的缓冲区。但是你调用usb_cache_string()的时候并没有指定一个明确的size,usb_cache_string()也就不知道你想要的那个字符串描述符有多大,于是它就采用了这么一个技巧,先申请一个足够大的缓冲区,这里是256字节,拿这个缓冲区去调用usb_string(),通过usb_string()的返回值会得到字符串描述符的真实大小,然后再拿这个真实的大小去申请一个缓冲区,并将大缓冲区里放的字符串描述符数据拷贝过来,这时那个大缓冲区当然就没什么利用价值了,于是再把它给释放掉。
第二就是申请那个小缓冲区的时候,使用的并不是usb_string()的返回值,而是多了1个字节,也就是说要从大缓冲区里多拷一个字节到小缓冲区里,为什么?这牵涉到C里字符串方面那个人见人愁鬼见鬼哭的代码杀手——字符串结束符。如果你说俺是危言耸听夸大其实,那只能说明你不是天才就是C代码写的少,咱不说C++,因为C++里更多的是用string。
字符串都需要那么一个结束符,这点是个正常人都知道的,但并不是每个正常人都能每时每刻的记得给字符串加上这么一个结束符。就像是个人都知道钞票不是万能的,但并不是每个人都知道:钞票不是万能的,有时还需要信用卡。可能你小心了1000次,但在第1001次的时候你给忘记了,你的代码就可能就可能挂了。不说玄乎儿的了,搞点实在的,给个例子
这程序简单直白,打印100个数,每行10个,你觉得它有什么毛病没?当然俺不是说算法上的问题,这本来就演示用的,你只需要看看它能不能得到预期的结果。
俺就直说了吧,这程序问题大大的,在第10行少了这么一句
就是说忘记将buf初始化了,传递给strcat的是一个没有初始化的buf,这个时候buf的内容都非0,strcat不知道它的结尾在哪里,它能猜到buf的开始,却猜不到buf的结束,多伤感啊。当然初始化的方法有多种,你可以使用memset将整个buf初始化为0,但是如果buf比较大的话,memset就比较的消耗CPU了,要时刻牢记咱们要创建一个节约型的社会,而这里buf[0] = '/0'就足够用了,所以没必要使用memset去全方位的搞一下。为了更好的说明问题,这里贴一下内核里对strcat的定义
strcat会从dest的起始位置开始去寻找一个字符串的结束符,只有找到,175行的while循环才会结束。“骰子已经掷了,就这样吧!”义无返顾的恺撒毅然跨越卢比肯河,一举击溃了庞培,最终占领了罗马古城。但是如果dest没有初始化过,义无反顾的strcat并不会有好运得到恺撒的结果。本来strcat的目的是将tmp追加到buf里字符串的后面,但是因为buf没有初始化,没有那么一个结束符,strcat就会一直的找下去,就算它运气好在哪儿停了下来,如果这个停下的位置超出了buf的范围,就会把src的数据写到不应该的地方,就可能会破坏其它可能很重要的数据,你的系统可能就挂掉了。
解决这种问题的方法很简单,就是记着遇到指针、数组什么的使用前首先统统的初始化掉,到最后真觉得哪里不必要影响性能了再去优化它,还是那句至理名言:过早优化便是罪。
问个大家都会笑的问题,这个字符串结束符具体是什么东东?C和C++里一般是指'/0',像上面的那句buf[0] = '/0'。这里再引用spec里的一句话:The UNICODE string descriptor is not NULL-terminated. 什么是NULL-terminated字符串?其实就是以'/0'结尾的字符串,咱们看看内核里对NULL的定义
春春是一个女人,但她又不仅仅是一个女人,0是一个整数,但它又不仅仅是一个整数。由于各种标准转换,0可以被用于作为任意的整型、浮点类型、指针。0的类型将由上下文来确定。典型情况下0被表示为一个适当大小的全零二进制位的模式。所以,无论NULL是定义为常数0还是((void *)0)这个零指针,NULL都是指的是0值,而不是非0值。而字符的'/0'和整数的0也可以通过转型来相互转换。再多说一点,'/0'是C语言定义的用'/'加8进制ASCII码来表示字符的一种方法,'/0'就是表示一个ASCII码值为0的字符。
所以更准确的说,NULL-terminated字符串就是以0值结束的字符串,那么spec的那句话说字符串描述符不是一个NULL-terminated字符串,意思也就是字符串描述符没有一个结束符,你从设备那里得到字符串之后得给它追加一个结束符。本来usb_string()里已经为buf追加好了,但是它返回的长度里还是没有包括进这个结束符的1个字节,所以usb_cache_string()为smallbuf申请内存的时候就得多准备那么一个字节,以便将buf里的那个结束符也给拷过来。现在就看看usb_string()的细节,定义在message.c里
763行,这几行做些例行检查,设备不能是挂起的,index也不能是0的,只要传递了指针就是需要检查的。
767行,初始化buf,usb_cache_string()并没有对这个buf初始化,所以这里必须要加上这么一步。当然usb_string()并不仅仅只有在usb_cache_string()里调用,可能会在很多地方调用到它,不过不管在哪里,这里谨慎起见,还是需要这么一步。
768行,申请一个256字节大小的缓冲区。前面一直强调说要初始化要初始化,怎么到这里俺就自己打自己一耳光,没有去初始化tbuf?这是因为没必要,为什么没必要,你看看usb_string()最后面的那一堆就明白了。
773行,struct usb_device里有have_langid和string_langid这么两个字段是和字符串描述符有关的,string_langid用来指定使用哪种语言,have_langid用来指定string_langid是否有效。如果have_langid为空,就说明没有指定使用哪种语言,那么获得的字符串描述符使用的是哪种语言就完全看设备的脸色和心情了。你可能会疑惑为什么当have_langid为空的时候,要在774行和793行调用两次usb_string_sub()?就像usb_string()是替usb_cache_string()做苦工的一样,usb_string_sub()是替usb_string()做苦工的,也就是很说usb_string()是靠usb_string_sub()去获得字符串描述符的,那问题就变成为什么have_langid为空的时候,要获取两遍的字符串描述符?
你可以比较一下两次调用usb_string_sub()的参数有什么区别,第一次调用的时候,语言ID和index都为0,第二次调用的时候就明确的指定了语言ID和index。这里的玄机就在index为0的时候,也就是0编号的字符串描述符是什么东东,前面只说了它有特殊的用途就甩一甩衣袖飘过了,飘得过初一飘不过十五,现在是怎么也飘不过了,必须得解释一下。
有困难要找居委会,有问题就要找协议,spec 9.6.7 里说了,编号0的字符串描述符包括了设备所支持的所有语言ID,对应的就是Table 9-15
第一次调用usb_string_sub()就是为了获得这张表,获得这张表做嘛用?你接着往下看。
775行,usb_string_sub()返回个负数,就表示没有获得这张表,没有取到0号字符串描述符。如果返回值比4要小,就表示获得的表里没有包含任何一个语言ID。要知道一个语言ID占用2个字节,还有前两个字节表示表的长度还有类型,所以得到的数据至少要为4,才能够得到一个有效的语言ID。如果返回值比4要大,就使用获得的数据的第3,4两个字节设置string_langid,同时设置have_langid为1。现在很明显了,773~791这一堆就是在你没有指定使用哪种语言的时候,去获取设备里默认使用的语言,也就是0号字符串描述符里的第一个语言ID所指定的语言。如果没有找到这个默认的语言ID,即usb_string_sub()返回值小于4的情况,就没有办法再去获得其它的字符串描述符了,因为没有指定语言啊,设备不知道你是要英语还是中文还是别的,它没有办法猜你的心思,应付不来这种无所适从的状况。
793行,使用指定的语言ID,或者前面获得的默认语言ID去获得想要的那个字符串描述符。现在看看定义在message.c里的usb_string_sub函数。
这个函数首先检查一下你的设备是不是属于那种有怪僻的,如果是一个没有毛病遵纪守法的合格设备,就调用usb_get_string()去帮着自己获得字符串描述符。USB_QUIRK_STRING_FETCH_255就是在include/linux/usb/quirks.h里定义的那些形形色色的毛病之一,《我是Hub》里详细的讲了设备的quirk,说了USB_QUIRK_STRING_FETCH_255就表示设备在获取字符串描述符的时候会crash。
usb_string_sub()的核心就是message.c里定义usb_get_string函数
我已经不记得这是第多少次遇到usb_control_msg()了,佛说前生500次的回眸才换得今生的一次擦肩而过,那咱们现在至少也成千上万次回眸了。老习惯,还是简单说一下它的一堆参数,wValue的高位字节表示描述符的类型,低位字节表示描述符的序号,所以有674行的(USB_DT_STRING << 8) + index,wIndex对于字符串描述符应该设置为使用语言的ID,所以有674行的langid,至于wLength,就是描述符的长度,对于字符串描述符很难有一个统一的确定的长度,所以一半来说上头儿传递过来的通常是一个比较大的255字节。
和获得设备描述符时一样,因为一些厂商搞出的设备古灵精怪的,可能需要多试几次才能成功。要容许设备犯错误,就像人总要犯错误一样,否则正确之路人满为患了。
还是回过头去看usb_string_sub函数,如果usb_get_string()成功的得到了期待的字符串描述符,则返回获得的字节数,如果这个数目小于2,就再读两个字节试试,要想明白这两个字节是什么内容,需要看看spec Table 9-16
Table 9-15是0号字符串描述符的格式,这个Table 9-16是其它字符串描述符的格式,很明显可以看到,它的前两个字节分别表示了长度和类型,如果读2个字节成功的话,就可以准确的获得这个字符串描述符的长度,然后可以再拿这个准确的长度去请求一次。
该尝试的都尝试了,现在看看716行,分析一下前面调用usb_get_string()的结果,如果几次尝试之后,它的返回值还是小于2,那就返回一个错误码。如果你的辛苦没有白费,rc大于等于2,说明终于获得了一个有效的字符串描述符。
717行,buf的前两个字节有一个为空时,也就是Table 9-16的前两个字节有一个为空时,调用了message.c里定义的usb_try_string_workarounds函数
这个函数的目的是从usb_get_string()返回的数据里计算出前面有效的那一部分的长度。它的核心就是686行的那个for循环,不过要搞清楚这个循环,还真不是一件容易的事儿,得有相当的理论功底,你看现在十七大一开,党章卖的多火爆,因为大家都知道了不能整天在那里促膝谈性了,要多学点理论。
不久之前刚说了字符串描述符使用的是UNICODE编码,其实UNICODE指的是包含了字符集、编码、字型等等很多规范的一整套系统,字符集仅仅描述符系统中存在哪些字符,并进行分类,并不涉及如何用数字编码表示的问题。UNICODE使用的编码形式主要就是两种UTF,即UTF-8和UTF-16。使用usb_get_string()获得的字符串使用的是UTF-16编码规则,而且是little-endpoint的,每个字符都需要使用两个字节来表示。你看这个for循环里newlength每次加2,就是表示每次处理一个字符的,但是要弄明白怎么处理的,还需要知道这两个字节分别是什么东东,这就不得不提及ASCII、ISO-8859-1等几个名词儿。
ASCII是用来表示英文的一种编码规范,表示的最大字符数为256,每个字符占1个字节,但是英文字符没那么多,一般来说128个也就够了(最高位为0),这就已经完全包括了控制字符、数字、大小写字母,还有其它一些符号。对于法语、西班牙语和德语之类的西欧语言都使用叫做ISO-8859-1的东东,它扩展了ASCII码的最高位,来表示像n上带有一个波浪线(241),和u上带有两个点(252)这样的字符。而Unicode的低字节,也就是在0到255上同ISO-8859-1完全一样,它接着使用剩余的数字,256到65535,扩展到表示其它语言的字符。所以可以说ISO-8859-1就是Unicode的子集,如果Unicode的高字节为0,则它表示的字符就和ISO-8859-1完全一样了。
有上面的理论垫底儿,咱们再看看这个for循环,newlength从2开始,是因为前两个字节应该是表示长度和类型的,这里只逐个儿对上面Table 9-16里的bString中的每个字符做处理。还要知道usb_get_string()得到的结果是little-endpoint的,所以buf[newlength]和buf[newlength + 1]分别表示一个字符的低字节和高字节,那么isprint(buf[newlength]就是用来判断一下这个Unicode字符的低字节是不是可以print的,如果不是,就没必要再往下循环了,后边儿的字符也不再看了,然后就到了690行的if,将newlength赋给buf[0],即bLength。length指向的是usb_get_string()返回的原始数据的长度,692行使用for循环计算出的有效长度将它给修改了。isprint在include/linux/ctype.h里定义,你可以去看看,这里就不多说了。
这个for循环终止的条件有两个,另外一个就是buf[newlength + 1],也就是这个Unicode字符的高字节不为0,这时它不存在对应的ISO-8859-1形式,为什么加上这个判断?你接着看。
usb_string_sub()的721行,buf[0]表示的就是bLength的值,如果它小于usb_get_string()获得的数据长度,说明这些数据里存在一些垃圾,有一些混进革命队伍里的反动势力,要把他们给揪出来排除掉。要知道这个rc是要做为真实有效的描述符长度返回的,所以这个时候需要将buf[0]赋给rc。
724行,每个Unicode字符需要使用两个字节来表示,所以rc必须为偶数,2的整数倍,如果为奇数,就得将最后那一个字节给抹掉,也就是将rc减1。咱们可以学习一下这里将一个数字转换为偶数时采用的技巧,(rc & 1)在rc为偶数时等于0,为奇数时等于1,再使用rc减去它,得到的就是一个偶数。
从716~725这几行,咱们应该看得出,在成功获得一个字符串描述符时,usb_string_sub()返回的是一个NULL-terminated字符串的长度,并没有涉及到结束符。牢记这一点,咱们回到usb_string函数的797行,先将size,也就是buf的大小减1,目的就是为结束符保留1个字节的位置。
798行,tbuf里保存的是GET_DESCRIPTOR请求返回的原始数据,err是usb_string_sub()的返回值,一切顺利的话,它表示的就是一个有效的描述符的大小。这里idx的初始值为0,而u的初始值为2,目的是从tbuf的第三个字节开始拷贝给buf,毕竟咱们的目的是获得字符串描述符里的那个bString,而不是整个描述符的内容。u每次都是增加2,这是因为采用的UTF-16是用两个字节表示一个字符的,所以循环一次要处理两个字节。
801行,这个if-else组合你可能比较糊涂,要搞清楚,还要蓦然回首看一下前面刚普及过的一些理论。tbuf里每个Unicode字符两个字节,又是little-endpoint的,所以801行就是判断这个Unicode字符的高位字节是不是为0,如果不为0,则ISO-8859-1里没有这个字符,就设置buf里的对应字符为‘?’。如果它的高位字节为0,就说明这个Unicode字符ISO-8859-1里也有,就直接将它的低位字节赋给buf。这么一个for循环下来,就将tbuf里的Unicode字符串转化成了buf里的ISO-8859-1字符串。
806行,为buf追加一个结束符。咱们这节也就结束了。
更多推荐
所有评论(0)