性能文章>LONG究竟有多长,从皇帝的新衣到海康SDK>

LONG究竟有多长,从皇帝的新衣到海康SDK原创

4年前
14466411

转眼之间初中毕业30年了,但我仍清楚的记得初中英语的一篇课文,题目叫《皇帝的新装》(“The king’s new clothes”)。这篇课文的前两句话是:”Long long ago, there was a king. He liked new clothes.“ 因为整篇文章不长,故事生动,文字优美,而且有很多经典的句式,所以当时老师要求要背诵这篇课文,于是学这篇文章的那几天,每天早自习时教室内外都可以听到”Long long ago, there was a king.“
image.png

从那之后,每当看到LONG这个单词,我便不由地想起当年反复背诵的”Long long ago, there was a king.“
LONG是英文中的常用词汇,使用场合很多,在计算机世界里也是如此。比如在经典的C语言中,便把long定义为语言本身的关键字,下图是截取C语言标准中的关键字部分,可以看到其中包含long。
image.png

在C语言中,long关键字的基本用法是用作对基本数据类型的修饰符(modifiers)。比如,在int类型前可以加上long表示更长的整数,在double前也可以加上long表示更长的浮点数。例如,下面是来自某C语言编程指南的示例。

short int smallInteger;
long int bigInteger;
signed int normalInteger;
unsigned int positiveInteger;

其中,long int可以简写为long。或者说,直接使用long定义变量时,long便代表long int。顺便说一下,本文只讨论long int,不讨论long double。
在2000年批准的C99标准中,新增了long long int的定义,用来表示比long int更长的整数。
说到这里,大家可能要问,long修饰后的整数到底有多长呢?
就像英文字典里没有定义long的精确长度,只是解释为”having considerable duration in time“一样,在C语言的标准里,也没有精确定义long和long long到底应该多长,只是做了一些限定。特意摘录C99标准中有关的文字如下:

The rank of long long int shall be greater than the rank of long int, which shall be greater than the rank of int, which shall be greater than the rank of short int, which shall be greater than the rank of signed char.

意思是说,要保证long long int比long int长,long int要比int长。
那么,long到底是多长呢?
简单回答,因为语言标准里没有明确定义,这个问题是因为环境不同而有差别的。读到这里,那些因为陪着小孩学编程的非软件专业朋友可能会笑话了,”你们搞软件的怎么搞的,这么个基本问题还模棱两可?“
是啊,作为一个做了二十多年软件的人,我也觉得这事搞得有点乱套,既然说是要成为工业标准,堂而皇之的称为”计算机科学“,那么该精确和严谨的就一定要尽可能精确和严谨,不能像安徒生写童话那样,用个”Long long ago, there was a king“就糊里糊涂糊弄过去了。

模糊就会导致分歧,导致误解,导致各种乱七八糟的麻烦。
话题拉回来,可能有很多理由,也可能没有什么理由,定义C标准的前辈没有明确定义long的长度,把这个问题留给了做编译器的,”你们看着办吧“。
对于做编译器的同行,这个问题一定是要确定的,因为编译器必须明确每个变量的大小,才知道要为它在内存中分配多大的空间。
编译好的程序要执行时,需要调用执行环境里的库(运行时库),传递参数时也必须以统一的约定来传。因此,这个问题又与操作系统有关。
糟糕的是,在不同的操作系统中,大家的定义可能是不同的。为了便于讨论,通常用数据模型(data model)来称呼这个问题。
不知道是幸运还是不幸,在32位环境下,Windows和Linux使用的都是ILP32模型,ILP分别代表int、long和指针(Pointer)三种类型,32表示它们都是32位的,也就是4个字节,因此这种模型也叫4/4/4模型。
糟糕的是,在64位系统中,Windows和Linux使用不同的数据模型。Windows64使用的是LLP64(4/4/8)模型,int和long都是32位,指针和long long是64位。
而Linux64使用的是LP64(4/8/8)模型,int是32位,long和指针都是64位。
注意了,睁大眼睛,到关键之处了。概而言之,在今天流行的两大操作系统平台上,32位下long的长度是一样的,而64位下是不一样的
说到这里,非软件专业的可能又要笑了,不仅非软件专业的,即使是软件专业的,这里也会有点坐不住吧,”要么就都一样,要么就都不一样,这么一会一样,一会不一样,不是要把大家搞晕么?!“

诚然如此啊,把全世界的程序员都请出来,能精确回答出这个差异的可能不到一半吧,各位看官,不妨把你心目中的这个比例做个留言,也算一个小调查吧。如果你自己不清楚,可以不说啊^-^
不管看官你清楚不清楚,至少国内一家很大公司的一些同行没有把这个问题搞清楚,有老雷亲身经历的案例为证,请继续阅读。

有人说,现代人呼吸的除了空气外,还有广告。套用这句话,始终注视着现代人的除了眼睛之外,还有摄像头。据说,仅在中国,就已经安装了2亿个摄像头,2019年还将新增1亿。这个数据可能不可靠,大家姑且听之。无论如何,满大街满世界的摄像头,大家都可以感受的到。
如此多的摄像头,自然造就了一些以开发和销售摄像头为主的企业,海康和大华是排名很靠前的两家。
今年年初,因为要在我们的识别软件中访问海康摄像头的图像,于是老雷的办公室里也准备了一批海康的产品,各种款式的摄像头和DVR。
image.png
当然,除了硬件外,还有软件,特别是海康的”设备网络SDK“。在海康的官网就有”设备网络SDK“的下载链接,不需要注册就可以直接下载,还是很友好的。下载页面上根据操作系统和32/64位分为四个链接,也很清晰。SDK的名字有点怪,不过也无妨。

  • 设备网络SDK_Win32
  • 设备网络SDK_Win64
  • 设备网络SDK_Linux32
  • 设备网络SDK_Linux64

因为我们的产品环境是64位Linux,所以我们选择的是SDK_Linux64。
本来这个工作是一位同事做的,我也没有太关心。在CentOS上用了一段时间,也没有出什么问题。但是最近在Ubuntu 19.04上面却出了问题,程序崩溃。因为有客户急着要用,客户是上帝,时间紧迫,老雷赶紧亲自上阵,上GDB!
幸运的是在Ubuntu上,这个问题可以稳定重现,在GDB中也可以。
不负所望,GDB抓到经典的段错误,上过老雷研习班的朋友,对SEGV都很熟悉了,在秀峰的中正行营里,老雷与各位谈笑风生,”拍案惊奇“!

Thread 2.22 "fireeye" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffebf9b5700 (LWP 15828)]
0x0000555555572822 in InputSource_IpCamera_HC::RealDataCallBack_V30 (
    lRealHandle=0, dwDataType=1, pBuffer=0x7fffe08a1390 "IMKH\002\001",
    dwBufSize=40, cookie=0x555abef0)
    at /home/fireeye/work/fireeye/source/InputIPCam.cpp:366
366			pThis->D4D(D4D_LEVEL_VERBOSE, "%s got callback: NET_DVR_SYSHEAD!", pThis->GetString());

排版有点乱,上个截图吧!
image.png

感谢海康的同行们,在发布版本中仍然包含了宝贵的调试信息。有了这些信息,让我们理解起来轻松了很多。
从下往上看,可以看到这个线程属于海康的libhpr模块。

in RecvOperation(int, pthread_mutex_t*, IO_DATA*) () from /opt/xedge/fireeye/bin/libhpr.so

从函数名来看,是用于接收摄像头数据的。
中间是一系列处理流的函数,估计是解码的,跳过不论,看最上面一帧,这是老雷公司(英文xedge,加速边缘计算之意,有意者请看xedge.ai)的函数。

InputSource_IpCamera_HC::RealDataCallBack_V30
 (lRealHandle=0, dwDataType=1, pBuffer=0x7fffe08a1390 "IMKH\002\001", dwBufSize=40, cookie=0x555abef0)
at /home/fireeye/work/fireeye/source/InputIPCam.cpp:366

从源代码的目录名中,可以看到fireeye,是火眼的意思,看了这个名字,有些INTEL的老朋友可能要笑,甚至误会,还是fire eye啊?必须解释一下,此fire eye非彼fire eye。今天的fire eye是真的和火有关,是识别火焰的。最近几年,与火结缘,做的项目都包含fire。就像当年喜欢long这个单词一样,老雷也是很喜欢fire这个单词。
闲话打住,继续debug。
细看上面崩溃的方法,名叫InputSource_IpCamera_HC::RealDataCallBack_V30。是InputSource_IpCamera_HC类的一个静态函数,是用作回掉函数传递给海康SDK的,调用处的代码如下:

iRet = NET_DVR_SetRealDataCallBack(lRealPlayHandle, InputSource_IpCamera_HC::RealDataCallBack_V30, this);

其中的NET_DVR_SetRealDataCallBack是海康SDK的公开函数。因为问题就出在这个函数上,有必要从SDK文档中摘录这个函数的原型如下:
BOOL NET_DVR_SetRealDataCallBack(LONG lRealHandle, fRealDataCallBack cbRealDataCallBack, DWORD dwUser);
注意这个函数的参数,第一个是LONG类型,第二个是函数指针,第三个是DWORD。
因为是多个问题搅在一起,所以有必要先交代一下,虽然我们使用的是Linux64版本的SDK,但是其中的很多文件和信息仍是与Windows版本共享的,比如这个原型定义,就有着非常深的Windows烙印。各位Linux铁粉看着一定非常不爽,怎么那么多大写?怎么那么多重定义类型?
是啊,今天大家都面临着两个环境,一个被认为垂垂老矣的Windows但又离不开它,丢它不掉,一个冉冉升起的Linux,被普遍看好,但一时半会又不能完全扶正。
于是乎,大家都要忙活两个环境,两套东西,有些一样,有些不一样,要努力相互兼容,但又不能完全做到,就这样痛并快乐着!
呼应前文,在64位下,微软的long和Linux的long是不一样长的,微软的long是32位,Linux的long是64位。
那么这个Linux64 SDK版本的函数中出现了LONG,到底是用的微软的long还是Linux的long呢?按道理,既然是Linux64的版本,就该遵循Linux64的规则,long是64位的。
但是,海康的同行们没有这样认为,他们仍然执拗地把LONG定义为int,使用了微软的套路:

    typedef  int                LONG;

有些看官可能不相信了,说老雷你没有搞错吧?人家也是大公司,不会出这样的低级错误吧?
老雷也怕冤枉了人家,特意反复确认,特别从Linux64 SDK的consoleDemo/include找到官方演示程序使用的头文件,截图如下:
image.png

这下相信了吧?
不得不说,如此暴力地在Linux64下把LONG定义为int是非常错误的一个决定,是非常不负责任的,有很多危害。不仅会导致大家认知的混乱,而且会导致源代码的冲突。要知道今天的每个项目都有很多的代码,大家都要考虑跨平台的问题。比如这个定义就与老雷的代码冲突了。因为老雷代码中的LONG是按LINUX64的约定是64位的,在Linux64下,LONG就定义为long(注意大小写差别)。
第一次看到海康SDK的这个定义时,我就以为是明显的”笔误“,将其纠正为long,但是这样会导致很多链接错误,ld程序找不到海康SDK中的函数。
这样看来,海康的同行编译SDK的库时也千真万确是把LONG定义为int的,谬矣!
为了解决这个问题,老雷不得不定义了一个HCLONG,代表是海康的LONG。海康的拼音缩写不是HK么,为什么用HC,因为SDK的名字叫HCNETxxx。
问题还只是开始,第一个参数是所谓的播放句柄,其值总是很小,所以32位或者64位不导致实质问题。导致问题的是第三个参数:
DWORD dwUser
这里没有使用LONG,但仍使用了微软风格的DWORD。DWORD代表double word,遵循的仍是微软的套路,在32位和64位系统中都是32位的。
问题大了,NET_DVR_SetRealDataCallBack是个设计回调函数的接口,第二个参数是回调函数,第三个参数是所谓的调用者数据。一般称为回调上下文,意思是告诉SDK,你回调我的函数时,把这个再传回给我。
第一次看到把这个参数定义为DWORD我顿感诧异!这怎么可以定义为DWORD呢?也是因为像LONG那样,搞不清楚长度么?
像这样的参数,一般要定义为void *这样的变长类型,在32位下为32位,在64位时为64位,因为调用者常常是要传指针的,在今天普遍使用C++语言的背景下,一般是传this指针的。
iRet = NET_DVR_SetRealDataCallBack(lRealPlayHandle, InputSource_IpCamera_HC::RealDataCallBack_V30, this);
这样的话,在回调函数中,便可以拿到this指针,然后再转给C++的方法,这样便从C代码又回到了C++代码,即:
C++代码 -> C回调 -> C++代码。
可是遇到了这样的SDK接口,还能传this指针么?
同事曾经这样写:

iRet = NET_DVR_SetRealDataCallBack(lRealPlayHandle, InputSource_IpCamera_HC::RealDataCallBack_V30, (DWORD)this);

这样行么?
编译器肯定同意的,“你要强转,我就给你转”。
运行时候呢?还真能工作,你说神不神?
是的,CentOS的版本就这样工作了几个月。但其实这是个巨大的陷阱,是隐藏了危机,对错误的纵容是非常危险的。
暂时不说CentOS下为啥能工作,先说Ubuntu下为啥崩溃吧?
道理很简单,this指针被截断了!本来是64位的指针,截一半,只传了32位。
请看编译器产生的调用代码:

=> 0x0000555555571f75 <+1209>:	mov    rax,QWORD PTR [rbp-0x2f8]
   0x0000555555571f7c <+1216>:	mov    edx,eax
   0x0000555555571f7e <+1218>:	mov    rax,QWORD PTR [rbp-0x2f8]
   0x0000555555571f85 <+1225>:	mov    eax,DWORD PTR [rax+0x8ec]
   0x0000555555571f8b <+1231>:	cdqe   
   0x0000555555571f8d <+1233>:	lea    rcx,[rip+0x83e]        # 0x5555555727d2 <InputSource_IpCamera_HC::RealDataCallBack_V30(int, unsigned int, unsigned char*, unsigned int, void*)>

晕汇编的不要怕,老雷一解释你就懂了。
第一条是从局部变量中读出对象指针(this),放到64位的rax寄存器中,看一下它的值:

(gdb) p this
$1 = (InputSource_IpCamera_HC * const) 0x5555555abef0

再看一下rax的值。

rax            0x5555555abef0      93824992591600

二者一摸一样,完整传递又省事又正确,多么完美啊!可是就因为这个错误定义的函数原型,不得不把好好的指针截断。

    mov    edx,eax

注意,这里使用的是eax和edx,都是32位的寄存器,也就是只把rax寄存器中的低32位传给了edx。
讲到这里,问题就很明显了,在回调函数中,只能取到this指针的低32位,一访问就崩溃了啊。
从上面的崩溃现场可以看到,第三个参数就是被截断了的指针。

0x0000555555572822 in InputSource_IpCamera_HC::RealDataCallBack_V30 (
    lRealHandle=0, dwDataType=1, pBuffer=0x7fffe08a1390 "IMKH\002\001",
    dwBufSize=40, cookie=0x555abef0)

说到这里,大家明白了Ubuntu下为什么崩溃,那么CentOS下为啥可以工作呢?
不是编译器的原因,编译器产生的代码是等价的。上调试器观察一下,就知道原因了。
在GDB下单步跟踪到调用指令处:

   0x00000000004b68a5 <+453>:	lea    rsi,[rip+0x6c4]        # 0x4b6f70 <_ZN23InputSource_IpCamera_HC20RealDataCallBack_V30EljPhjj>
   0x00000000004b68ac <+460>:	movsxd rdi,eax
   0x00000000004b68af <+463>:	mov    edx,ebx
=> 0x00000000004b68b1 <+465>:	call   0x469140 <NET_DVR_SetRealDataCallBack@plt>

观察寄存器的值:

(gdb) info registers
rax            0x0	0
rbx            0x725b60	7494496
rcx            0x0	0
rdx            0x725b60	7494496

其中rdx中是this指针的内容,结合观察变量值,可以发现 在CentOS下运行时,this指针的值总是比较小,高32位总是0,只用低32位就够了
怎么会这样?一种解释是巧合,另一种解释是CentOS下的内存分配策略使然。
再回到Ubuntu,按说在Ubuntu 64下使用这个SDK的用户应该也不少,其他同行难道没有遇到这个问题么?难道他们都觉得很好,我是第一个说皇帝没有穿衣服的那个不懂事的孩子么?
image.png

看海康官方的示例代码,演示了调用这个API,但是最后一个参数传递的是0:

iRet = NET_DVR_SetRealDataCallBack(lRealPlayHandle, g_HikDataCallBack, 0);

这是故意的吗?
另一种解释是大家都只用C语言,使用全局变量,不用this指针,但是这样做支持多实例会很麻烦的,让代码的伸缩性很差,也很难看。

怎么解决这个问题呢?同事说用个映射表吧?那意味着本来非常简洁优雅的代码会长上很多赘肉,难以让人接受。不到万不得已,不能用此下策。还是要要感谢调试器,在GDB的帮助下,我发现一个名为NET_DVR_SetRealDataCallBackEx的函数。根据GDB显示的原型信息,它的最后一个参数正好是我所希望的void *。看来曾经有人发现“皇帝新装”的问题,也有人修正过。可是查遍官方文档,没有这个Ex版本函数的说明,在Linux版本的头文件中,也没有这个函数的定义。
怎么解决呢?自己写函数声明吧,编译连接,没有问题,测试运行,正常工作了!

读到这里,不知道各位看官作何感想?欢迎各位留言。创业初期,异常忙碌,借周末时间,忙里偷闲,撰写拙文,不敢奢望行业巨头能否虚心接受和改正,只希望对各位同行有所启发,如有小补,亦感深慰。

点赞收藏
张银奎

前英特尔软件架构师

请先登录,查看4条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
11
4