最近的项目中,使用了Gstreamer的rtmpsink来推送RTMP到流媒体服务器,但是总是遇到奇怪的段错误,读了一下rtmpsink的源码。发现虽然有问题,但是可能会有编码上的问题,但是还不至于有段错误。
于是,觉得有可能是rtmpsink使用的librtmp库的问题。
coredump
使用
coredumpctl gdb
这里记录一下,coredumpctl这个命令。
coredumpctl使用list可以显示崩溃的coredump的列表,之后使用gdb加载的时候,可以直接通过PID来加载指定的coredump。
如,我们执行
coredumpctl list
返回了:
Wed 2024-12-18 15:11:31 CST 1422072 0 0 11 present /bin/bash
Wed 2024-12-18 16:46:17 CST 1427297 0 0 11 present /bin/bash
Wed 2024-12-18 17:20:12 CST 1429745 0 0 11 present /bin/bash
Wed 2024-12-18 17:31:28 CST 1430341 0 0 11 present /bin/ls
Wed 2024-12-18 18:35:07 CST 1438184 0 0 11 present /bin/yes
Thu 2024-12-19 09:14:29 CST 1477445 0 0 11 present /bin/bash
,其中第一列是时间,第二列(CST之后)那列,就是PID号。
比如我们这时候执行:
coredumpctl gdb 1429745
,就可以把12月18号晚上五点二十那次崩溃的coredump加载进来。
librtmp
我们加载了gstreamer应用的coredump,之后通过gdb的bt命令,看到了崩溃的函数位于librtmp.so的RTMP_Write函数里面。
因为默认安装的librtmp没有安装debug-info,所以通过debuginfo-install librtmp来安装,之后就可以看到具体的调试信息。
安装之后,我们再次加载,通过bt查看堆栈,就看到了具体信息:
Program terminated with signal SIGSEGV, Segmentation fault.
--Type <RET> for more, q to quit, c to continue without paging--
#0 0x00007f646a400489 in ?? () from /usr/lib64/libc.so.6
[Current thread is 1 (LWP 1477490)]
(gdb) bt
#0 0x00007f646a400489 in () at /usr/lib64/libc.so.6
#1 0x00007f646803ac00 in RTMP_Write
(r=0x11c4000, buf=0x7f64540849d5 "\353Lu\251\215\236}D^M\253<@3\005\005\003{\036", <incomplete sequence \346>, size=65507) at rtmp.c:5165
#2 0x00007f6469acd36c in gst_rtmp_sink_render (bsink=0x11b5880, buf=0x7f645403d360) at gstrtmpsink.c:271
#3 0x00007f64699f60ee in () at /usr/lib64/libgstbase-1.0.so.0
#4 0x00007f64699f6e60 in () at /usr/lib64/libgstbase-1.0.so.0
#5 0x00007f6469f3f21b in () at /usr/lib64/libgstreamer-1.0.so.0
#6 0x00007f6469f47343 in gst_pad_push () at /usr/lib64/libgstreamer-1.0.so.0
#7 0x00007f64690862a9 in () at /usr/lib64/gstreamer-1.0/libgstcoreelements.so
#8 0x00007f6469f3f21b in () at /usr/lib64/libgstreamer-1.0.so.0
此时,我们输入f 1,进入RTMP_Write的栈帧,得到了:
(gdb) f 1
#1 0x00007f646803ac00 in RTMP_Write (r=0x11c4000,
buf=0x7f64540849d5 "\353Lu\251\215\236}D^M\253<@3\005\005\003{\036", <incomplete sequence \346>,
size=65507) at rtmp.c:5165
5165 memcpy(enc, buf, num);
(gdb)
看到这里,很明显这几个参数有内存越界了,所以我们打印变量值验证:
(gdb) f 1
#1 0x00007f646803ac00 in RTMP_Write (r=0x11c4000,
buf=0x7f64540849d5 "\353Lu\251\215\236}D^M\253<@3\005\005\003{\036", <incomplete sequence \346>,
size=65507) at rtmp.c:5165
5165 memcpy(enc, buf, num);
(gdb) p enc
$1 = 0x7f6454028bd2 ""
(gdb) p buf
$2 = 0x7f64540849d5 "\353Lu\251\215\236}D^M\253<@3\005\005\003{\036", <incomplete sequence \346>
(gdb) p num
$3 = -2
(gdb)
num竟然是-2。
我们显示一下当前的源代码,看看这个-2是怎么来的。
(gdb) p num
$3 = -2
(gdb) l
5160 enc = pkt->m_body + pkt->m_nBytesRead;
5161 }
5162 num = pkt->m_nBodySize - pkt->m_nBytesRead;
5163 if (num > s2)
5164 num = s2;
5165 memcpy(enc, buf, num);
5166 pkt->m_nBytesRead += num;
5167 s2 -= num;
5168 buf += num;
5169 if (pkt->m_nBytesRead == pkt->m_nBodySize)
(gdb) p s2
$4 = -2
(gdb)
原来,s2就是-2了。
而具体这个s2为什么会变成-2,就可以通过源码看一下了。
我们打开这个版本的librtmp的源码,找到RTMP_Write函数的实现:
1 int
2 RTMP_Write(RTMP *r, const char *buf, int size)
3 {
4 RTMPPacket *pkt = &r->m_write;
5 char *pend, *enc;
6 int s2 = size, ret, num;
7
8 pkt->m_nChannel = 0x04; /* source channel */
9 pkt->m_nInfoField2 = r->m_stream_id;
10
11 while (s2)
12 {
13 if (!pkt->m_nBytesRead)
14 {
15 if (size < 11) {
16 /* FLV pkt too small */
17 return 0;
18 }
19
20 if (buf[0] == 'F' && buf[1] == 'L' && buf[2] == 'V')
21 {
22 buf += 13;
23 s2 -= 13;
24 }
25
26 pkt->m_packetType = *buf++;
27 pkt->m_nBodySize = AMF_DecodeInt24(buf);
28 buf += 3;
29 pkt->m_nTimeStamp = AMF_DecodeInt24(buf);
30 buf += 3;
31 pkt->m_nTimeStamp |= *buf++ << 24;
32 buf += 3;
33 s2 -= 11;
34
35 if (((pkt->m_packetType == RTMP_PACKET_TYPE_AUDIO
36 || pkt->m_packetType == RTMP_PACKET_TYPE_VIDEO) &&
37 !pkt->m_nTimeStamp) || pkt->m_packetType == RTMP_PACKET_TYPE_INFO)
38 {
39 pkt->m_headerType = RTMP_PACKET_SIZE_LARGE;
40 if (pkt->m_packetType == RTMP_PACKET_TYPE_INFO)
41 pkt->m_nBodySize += 16;
42 }
43 else
44 {
45 pkt->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
46 }
47
48 if (!RTMPPacket_Alloc(pkt, pkt->m_nBodySize))
49 {
50 RTMP_Log(RTMP_LOGDEBUG, "%s, failed to allocate packet", __FUNCTION__);
51 return FALSE;
52 }
53 enc = pkt->m_body;
54 pend = enc + pkt->m_nBodySize;
55 if (pkt->m_packetType == RTMP_PACKET_TYPE_INFO)
56 {
57 enc = AMF_EncodeString(enc, pend, &av_setDataFrame);
58 pkt->m_nBytesRead = enc - pkt->m_body;
59 }
60 }
61 else
62 {
63 enc = pkt->m_body + pkt->m_nBytesRead;
64 }
65 num = pkt->m_nBodySize - pkt->m_nBytesRead;
66 if (num > s2)
67 num = s2;
68 memcpy(enc, buf, num);
69 pkt->m_nBytesRead += num;
70 s2 -= num;
71 buf += num;
72 if (pkt->m_nBytesRead == pkt->m_nBodySize)
73 {
74 ret = RTMP_SendPacket(r, pkt, FALSE);
75 RTMPPacket_Free(pkt);
76 pkt->m_nBytesRead = 0;
77 if (!ret)
78 return -1;
79 buf += 4;
80 s2 -= 4;
81 if (s2 < 0)
82 break;
83 }
84 }
85 return size+s2;
86 }
这个实现读下来,嗯……真是一言难尽。
我们可以清楚地看到,为什么s2会变成-2。
因为:完全没有验证过啊!
虽然循环开头认真地校验了size的值:
15 if (size < 11) {
16 /* FLV pkt too small */
17 return 0;
18 }
可是,这里的size从来也没有更改过,更改的是s2!当然就解包越界就崩溃了。
rtmpsink
而且,通过这个函数实现,我们还发现,rtmpsink里面的代码也是问题很大的。
我们看到这个函数的末尾:
81 if (s2 < 0)
82 break;
83 }
84 }
85 return size+s2;
86 }
这个返回值,就很不明确。
s2小于0的时候,返回了与原缓冲区大小的和。我们只能猜测这是让我们上层的实现,通过判断这里是不是小于输入的size,来表示这里还差多少字节的解析拼包吧!
但是,rtmpsink里的实现:
263 if (sink->have_write_error)
264 goto write_failed;
265
266 GST_LOG_OBJECT (sink, "Sending %" G_GSIZE_FORMAT " bytes to RTMP server",
267 gst_buffer_get_size (buf));
268
269 gst_buffer_map (buf, &map, GST_MAP_READ);
270
271 if (RTMP_Write (sink->rtmp, (char *) map.data, map.size) <= 0)
272 goto write_failed;
273
274 gst_buffer_unmap (buf, &map);
275 if (need_unref)
276 gst_buffer_unref (buf);
277
278 return GST_FLOW_OK;
完全没处理这种边界情况。只要是>0,就都返回GST_FLOW_OK。
至此,一个崩溃问题就分析差不多,需要从几个方面完善地修复bug了。