腾讯会议为何不闪即退
腾讯会议为何不闪即退
2023/05/02
◐
最近一段时间里,我的背包里总是装着一新一旧两台笔记本,它们都是灰色的,屏幕都是14英寸,外壳的长度是一样的。论差异,新的比旧的明显薄很多,也轻很多。
YOURLAND
除了外表的差别外,它们更大的差别是“内心”:旧的是Wintel(Windows + X86),新的是ARM + Linux。旧的叫ThinkPad,新的叫幽兰。
用了几十年Wintel之后,我决意要迁移到新的ARM + Linux了。要实现这个迁移,数量众多的应用软件是最大的困难。比如,视频会议软件的问题就不大好解决。以Zoom和腾讯会议这两个著名的会议系统软件为例,Zoom的网站上对LINUX系统只有X86版本供下载,而腾讯会议虽然有ARM64的版本,但是装了之后不闪即退。
具体来说,从腾讯会议官网下载腾讯会议的ARM64版本客户端后,可以在Downloads目录中找打一个大约146MB的安装包(deb文件):
TencentMeeting_0300000000_3.14.0.402_arm64_default.publish.deb
打开终端窗口,执行如下命令安装:
sudo dpkg -i ~/Downaloads/TencentMeeting_0300000000_3.14.0.402_arm64_default.publish.deb
安装过程是比较顺利的,安装后,应用程序列表中会新增一个腾讯会议的图标,名叫WemeetApp。
但是点击这个图标试图启动它时,根本没有看到任何腾讯会议的界面,而是回到空的桌面,根据多年的经验,看起来是启动时失败了,失败的时间点可能比较早,所以根本没有机会显示出任何窗口,哪怕是小小的一个对话框。
几周前就听小伙伴说起过这个问题,但是我一直没有空深究。这几天五一小长假,我特意分出一段时间来调查这个问题。
图形方式启动看到的信息太有限了,为了便于分析,我打开一个终端窗口,切换到存放第三方程序的/opt目录,ls一下,果然看到wemeet目录,进去后看到几个子目录,还有一个脚本文件:wemeetapp.sh
bin lib plugins resources translations wemeetapp.sh wemeet.png wemeet.svg
读一下这个这个脚本,看起来就是用来启动应用程序的。
手工执行这个脚本,可以看到一些错误信息了:
开始的四行错误信息是关于图形子系统(DRI)的,我最初以为是这几个错误导致无法启动,但是网上搜索一番后得知,这些错误信息可能并无大碍,在多年前就有很多讨论了。接下来的LoadCustomFont(加载定制字体)信息,也证明了这一点,说明在DRI错误后程序的逻辑仍在前进。
两行YaHei(雅黑)信息后,有一行是:
Failed to open file ‘/opt/wemeet/bin/xcast.conf’
无法打开配置文件,我核对了一下,的确没有这个配置文件。
接下来的一系列信息都有XNN字样,感觉是在初始化腾讯会议的内部设施了,比如XNNContext和XNNRTResource。
或许再跑几步,界面就出现了,但这时致命的事件发生了:
Segmentation fault(段错误)
段错误是Linux下应用程序崩溃的最常见原因,这个错误的名字本身也是错误,它的真正含义其实大多时候都是页错误,相当于Windows下的访问违例(access violation),因为历史原因叫了这个名字,不好改了。
另外值得注意的是,虽然段错误信息后,有core dumped字样,但是其实默认并不会产生core文件。
执行如下步骤,
可以启用core文件:
ulimit -c unlimited
echo /var/tmp/%p.core
启用后,再重现错误,果然在/var/tmp下有core文件了:
geduer@ulan:/var/tmp$ ls
6770.core
接下来启动gdb
分析core文件:
gdb --core /var/tmp/6770.core /opt/wemeet/bin/wemeetapp
GDB启动后,列出很多线程后,显示出如下崩溃现场:
执行thread apply all bt观察各个线程的执行情况,可以看到,进程里一共有45个线程,很多线程已经是就绪状态,进入等待函数。
28号线程看起来是腾讯会议的业务线程,代码所属的模块名为libwemeet_base.so,它在调用select系统调用等待数据。
Thread 28 (Thread 0x7f44ff8fd0 (LWP 6800)):
#0 0x0000007fb1844620 in __GI___select (nfds=<optimized out>, readfds=0x7f44ff85d8, writefds=0x0, exceptfds=0x0, timeout=0x7f44ff85c8) at ../sysdeps/unix/sysv/linux/select.c:53
#1 0x0000007faf863c38 in ?? () from /opt/wemeet/lib/libwemeet_base.so
#2 0x0000007faf867f4c in ?? () from /opt/wemeet/lib/libwemeet_base.so
#3 0x0000007faf867e50 in ?? () from /opt/wemeet/lib/libwemeet_base.so
#4 0x0000007faf867df4 in ?? () from /opt/wemeet/lib/libwemeet_base.so
#5 0x0000007faf867d94 in ?? () from /opt/wemeet/lib/libwemeet_base.so
#6 0x0000007faf867b30 in ?? () from /opt/wemeet/lib/libwemeet_base.so
#7 0x0000007fb1a88fac in ?? () from /lib/aarch64-linux-gnu/libstdc++.so.6
#8 0x0000007fa36a4624 in start_thread (arg=0x7fb1a88f90) at pthread_create.c:477
#9 0x0000007fb184b49c in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone.S:78
再回到崩溃现场
执行bt命令观察调用栈,可以看到是在调用eglGetDisplay时触发了钩子逻辑:
(gdb) bt
#0 0x0000007f9efc508c in fixup_x11_display (display=0x1) at ../hook/hook.c:462
#1 0x0000007f9efc5104 in eglGetDisplay (display_id=<optimized out>) at ../hook/hook.c:485
#2 0x0000007fa1beccd8 in ?? () from /opt/wemeet/lib/libxcast.so
查阅EGL的官方文档,可以看到eglGetDisplay函数的详细说明。某种程度来说,这个函数与Windows的获取屏幕DC(GetDC(NULL))差不多,就是获取整个屏幕的显示上下文。
https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetDisplay.xhtml
崩溃发生在fixup_x11_display函数中,它的源文件名叫hook.c,行号为462行。腾讯没有剥离这个模块的符号,让我们可以看到源代码信息,这可能有两个原因:
01/
这个模块经常出问题
02/
这个模块包含的不是腾讯会议的核心逻辑
因为有符号,所以我们可以看当前函数的参数信息:
(gdb) info args
display = 0x1
观察这个参数的类型,可以看到它是指向一个局大结构体的指针:
(gdb) pt display
type = struct _XDisplay {
XExtData *ext_data;
struct _XFreeFuncs *free_funcs;
int fd;
int conn_checker;
int proto_major_version;
int proto_minor_version;
char *vendor;
XID resource_base;
XID resource_mask;
XID resource_id;
int resource_shift;
XID (*resource_alloc)(struct _XDisplay *);
int byte_order;
int bitmap_unit;
int bitmap_pad;
int bitmap_bit_order;
int nformats;
ScreenFormat *pixmap_format;
int vnumber;
int release;
struct _XSQEvent *head;
struct _XSQEvent *tail;
int qlen;
unsigned long last_request_read;
unsigned long request;
char *last_req;
char *buffer;
char *bufptr;
char *bufmax;
unsigned int max_request_size;
struct _XrmHashBucketRec *db;
--Type <RET> for more, q to quit, c to continue without paging--
int (*synchandler)(struct _XDisplay *);
char *display_name;
int default_screen;
int nscreens;
Screen *screens;
unsigned long motion_buffer;
volatile unsigned long flags;
int min_keycode;
int max_keycode;
KeySym *keysyms;
XModifierKeymap *modifiermap;
int keysyms_per_keycode;
char *xdefaults;
char *scratch_buffer;
unsigned long scratch_length;
int ext_number;
struct _XExten *ext_procs;
int (*event_vec[128])(Display *, XEvent *, xEvent *);
int (*wire_vec[128])(Display *, XEvent *, xEvent *);
KeySym lock_meaning;
struct _XLockInfo *lock;
struct _XInternalAsync *async_handlers;
unsigned long bigreq_size;
struct _XLockPtrs *lock_fns;
void (*idlist_alloc)(Display *, XID *, int);
struct _XKeytrans *key_bindings;
Font cursor_font;
struct _XDisplayAtoms *atoms;
unsigned int mode_switch;
unsigned int num_lock;
struct _XContextDB *context_db;
int (**error_vec)(Display *, XErrorEvent *, xError *);
--Type <RET> for more, q to quit, c to continue without paging--
struct {
XPointer defaultCCCs;
XPointer clientCmaps;
XPointer perVisualIntensityMaps;
} cms;
struct _XIMFilter *im_filters;
struct _XSQEvent *qfree;
unsigned long next_event_serial_num;
struct _XExten *flushes;
struct _XConnectionInfo *im_fd_info;
int im_fd_length;
struct _XConnWatchInfo *conn_watchers;
int watcher_count;
XPointer filedes;
int (*savedsynchandler)(Display *);
XID resource_max;
int xcmisc_opcode;
struct _XkbInfoRec *xkb_info;
struct _XtransConnInfo *trans_conn;
struct _X11XCBPrivate *xcb;
unsigned int next_cookie;
int (*generic_event_vec[128])(Display *, XGenericEventCookie *, xEvent *);
int (*generic_event_copy_vec[128])(Display *, XGenericEventCookie *, XGenericEventCookie *);
void *cookiejar;
} *
既然是指针,那么这个指针的值就很古怪,是1。在Linux这样的软件世界里,1不是有效地址,属于空指针。
那么这个1是GDB的显示错误,
还是的确如此呢?
我们可以尝试通过寄存器来核对。
(gdb) info registers
x0 0x0 0
x1 0x7f9efc54e0 548128183520
x2 0x53 83
x3 0x5655 22101
x4 0x8 8
x5 0x20 32
x6 0x68437364466b6664 7512975477899486820
x7 0x7f7f7f7f7f7f7f7f 9187201950435737471
x8 0x101010101010101 72340172838076673
x9 0x7eec000bb0 545125305264
x10 0x0 0
x11 0x30 48
x12 0x7f9efc4260 548128178784
x13 0x0 0
x14 0x0 0
x15 0x20 32
x16 0x7f9efd6060 548128252000
x17 0x7fb17afad0 548438473424
x18 0x0 0
x19 0x1 1
x20 0x0 0
x21 0x0 0
x22 0x7fa2625f59 548185202521
x23 0x7fa2625f83 548185202563
x24 0x7eec000b60 545125305184
x25 0x7fa2896000 548187758592
x26 0x7efa7fbfd0 545368555472
x27 0x7fa36c9000 548202647552
x28 0x7efa7fbfd0 545368555472
x29 0x7efa7fb640 545368553024
x30 0x7f9efc5074 548128182388
sp 0x7efa7fb640 0x7efa7fb640
pc 0x7f9efc508c 0x7f9efc508c <fixup_x11_display+52>
cpsr 0x20001000 [ EL=0 SSBS C ]
fpsr 0x1f 31
fpcr 0x0 0
寄存器x19的值是1,
它是参数么?
执行info frame观察帧信息:
(gdb) info frame
Stack level 0, frame at 0x7efa7fb670:
pc = 0x7f9efc508c in fixup_x11_display (../hook/hook.c:462); saved pc = 0x7f9efc5104
called by frame at 0x7efa7fb680
source language c.
Arglist at 0x7efa7fb640, args: display=0x1
Locals at 0x7efa7fb640, Previous frame's sp is 0x7efa7fb670
Saved registers:
x19 at 0x7efa7fb650, x20 at 0x7efa7fb658, x29 at 0x7efa7fb640, x30 at 0x7efa7fb648
x19的本来值保存到内存了,本函数里可能使用它来记录display参数。
是否真的如此,可以看汇编指令:
0x0000007f9efc5064 <+12>: mov x19, x0
(gdb) disassemble
Dump of assembler code for function fixup_x11_display:
0x0000007f9efc5058 <+0>: stp x29, x30, [sp, #-48]!
0x0000007f9efc505c <+4>: mov x29, sp
0x0000007f9efc5060 <+8>: stp x19, x20, [sp, #16]
0x0000007f9efc5064 <+12>: mov x19, x0
0x0000007f9efc5068 <+16>: adrp x0, 0x7f9efc5000 <register_tm_clones+48>
0x0000007f9efc506c <+20>: add x0, x0, #0x4c8
0x0000007f9efc5070 <+24>: bl 0x7f9efc4db0 <getenv@plt>
0x0000007f9efc5074 <+28>: cbz x0, 0x7f9efc5088 <fixup_x11_display+48>
0x0000007f9efc5078 <+32>: mov x0, x19
0x0000007f9efc507c <+36>: ldp x19, x20, [sp, #16]
0x0000007f9efc5080 <+40>: ldp x29, x30, [sp], #48
0x0000007f9efc5084 <+44>: ret
0x0000007f9efc5088 <+48>: cbz x19, 0x7f9efc5078 <fixup_x11_display+32>
=> 0x0000007f9efc508c <+52>: ldr x0, [x19, #2408]
0x0000007f9efc5090 <+56>: cbnz x0, 0x7f9efc5078 <fixup_x11_display+32>
0x0000007f9efc5094 <+60>: str x21, [sp, #32]
0x0000007f9efc5098 <+64>: adrp x21, 0x7f9efd6000 <exit@got.plt>
0x0000007f9efc509c <+68>: add x20, x21, #0x90
0x0000007f9efc50a0 <+72>: mov x0, x20
0x0000007f9efc50a4 <+76>: bl 0x7f9efc4dd0 <pthread_mutex_lock@plt>
0x0000007f9efc50a8 <+80>: ldr x0, [x19, #216]
0x0000007f9efc50ac <+84>: bl 0x7f9efc4d60 <XOpenDisplay@plt>
0x0000007f9efc50b0 <+88>: mov x19, x0
0x0000007f9efc50b4 <+92>: add x2, x20, #0x30
0x0000007f9efc50b8 <+96>: mov x0, #0x0 // #0
0x0000007f9efc50bc <+100>: b 0x7f9efc50c4 <fixup_x11_display+108>
0x0000007f9efc50c0 <+104>: b.eq 0x7f9efc50dc <fixup_x11_display+132> // b.none
0x0000007f9efc50c4 <+108>: ldr x1, [x2, x0, lsl #3]
0x0000007f9efc50c8 <+112>: mov w3, w0
0x0000007f9efc50cc <+116>: add x0, x0, #0x1
0x0000007f9efc50d0 <+120>: cmp x0, #0x20
--Type <RET> for more, q to quit, c to continue without paging--
0x0000007f9efc50d4 <+124>: cbnz x1, 0x7f9efc50c0 <fixup_x11_display+104>
0x0000007f9efc50d8 <+128>: str x19, [x2, w3, sxtw #3]
0x0000007f9efc50dc <+132>: add x0, x21, #0x90
0x0000007f9efc50e0 <+136>: bl 0x7f9efc4de0 <pthread_mutex_unlock@plt>
0x0000007f9efc50e4 <+140>: mov x0, x19
0x0000007f9efc50e8 <+144>: ldp x19, x20, [sp, #16]
0x0000007f9efc50ec <+148>: ldr x21, [sp, #32]
0x0000007f9efc50f0 <+152>: ldp x29, x30, [sp], #48
0x0000007f9efc50f4 <+156>: ret
End of assembler dump.
看上面的反汇编结果,果然在第4行处把本来放在x0里的参数转存到了x19。
再往下看,带有=>标志的那一行是引发崩溃的指令:
=> 0x0000007f9efc508c <+52>: ldr x0, [x19, #2408]
这条指令果然使用了x19,在访问x19所指向结构体的偏移2408的字段。
如此看来,腾讯会议闪退是因为获取屏幕句柄时,触发了内部的钩子函数fixup_x11_display。而fixup_x11_display内部没有很好的检查传进来的XDisplay指针是否有效(并不是不为0的指针都有效),在访问XDisplay大结构体的+2408字段时引发段错误而导致整个进程终止。
相对于了老牌的WinTel,新的ARM + Linux还很年轻,有些设施还不成熟,但对于程序员来说,ARM + Linux代表着未来,是大势所趋,是机遇所在。也正因为它们年轻,所以充满着各种有趣的问题。仿佛像英姿勃发的少年,虽然有时表现单纯,甚至鲁莽,但是性情天真,充满魅力。
ARM + Linux
腾讯会议为何不闪即退
2023/05/02
盛格塾是格蠹科技旗下的知识分享平台,是以“格物致知”为教育理念的现代私塾。
本着为先圣继绝学的思想,盛格塾努力将传统文化中的精华与现代科技密切结合,以传统文化和人文情怀阐释现代科技,用现代科技传播传统文化。
访问方式
手机端:微信小程序搜索“盛格塾”
电脑端:下载Nano Code社区版客户端
https://nanocode.cn/#/download
格友公众号
盛格塾小程序
往期 · 精彩推荐