美文网首页
avio_open2崩溃分析

avio_open2崩溃分析

作者: 星星杨 | 来源:发表于2022-12-02 10:07 被阅读0次

问题起因:
在输入错误url情况下,avio_open2会报错;
其实最简单的办法就是把ffmpeg源码拖进工程项目跑,就能发现具体出错在哪里了,但是我尝试了很多次,把源文件拖进去,由于各种错误,命名冲突,文件路径问题,arm,x386等不同指令集下的代码都掺杂到一起,无奈只能反向推测结合源码找问题了;



通过符号断点,可以发现报错的地方其实是在rtmp_open函数里面的av_dict_free;这个opts就是我们avio_open2里面的最后一个参数;


崩溃堆栈

苦于没地方找问题,只能通过源码找问题了;

avio_open

一个和avio_open2()“长得很像”的函数avio_open(),应该是avio_open2()的早期版本。avio_open()比avio_open2()少了最后2个参数。而它前面几个参数的含义和avio_open2()是一样的。从源代码中可以看出,avio_open()内部调用了avio_open2(),并且把avio_open2()的后2个参数设置成了NULL,因此它的功能实际上和avio_open2()是一样的。avio_open()源代码如下所示

int avio_open(AVIOContext **s, const char *filename, int flags)  
{  
    return avio_open2(s, filename, flags, NULL, NULL);  
} 

avio_check

avio_check里面内部先调用ffurl_alloc,接下来是存在url_check就执行,否则执行ffurl_connect;

int avio_check(const char *url, int flags)
{
    URLContext *h;
    int ret = ffurl_alloc(&h, url, flags, NULL);
    if (ret < 0)
        return ret;

    if (h->prot->url_check) {
        ret = h->prot->url_check(h, flags);
    } else {
        ret = ffurl_connect(h, NULL);
        if (ret >= 0)
            ret = flags;
    }

    ffurl_close(h);
    return ret;
}

avio_open2

老版的avio_open2源码,摘自网上;

int avio_open2(AVIOContext **s, const char *filename, int flags,  
               const AVIOInterruptCB *int_cb, AVDictionary **options)  
{  
    URLContext *h;  
    int err;  

    err = ffurl_open(&h, filename, flags, int_cb, options);  
    if (err < 0)  
        return err;  
    err = ffio_fdopen(s, h);  
    if (err < 0) {  
        ffurl_close(h);  
        return err;  
    }  
    return 0;  
}  

下面是新版的avio_open2,摘自ffmpeg4.3.1源码;
直接调用ffio_open_whitelist,ffio_open_whitelist包括ffurl_open_whitelist及ffio_fdopen;
ffurl_open_whitelist跟之前的ffurl_open很像,只是多了whitelist跟blacklist来验证url的规范性、完整性,所以我们可以从这里入手来看看有没有解决办法;

int avio_open2(AVIOContext **s, const char *filename, int flags,
               const AVIOInterruptCB *int_cb, AVDictionary **options)
{
    return ffio_open_whitelist(s, filename, flags, int_cb, options, NULL, NULL);
}

int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char *blacklist
                        )
{
    URLContext *h;
    int err;

    *s = NULL;

    err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);
    if (err < 0)
        return err;
    err = ffio_fdopen(s, h);
    if (err < 0) {
        ffurl_close(h);
        return err;
    }
    return 0;
}

ffurl_open_whitelist

ffurl_open_whitelist内部调用了ffurl_allocffurl_connect,这个函数内部就跟avio_check很像了,只是中间加了一大堆options处理的操作;
如果prot->url_check为空的情况下,ffurl_open_whitelist === avio_check;

int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char* blacklist,
                         URLContext *parent)
{
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;
    int ret = ffurl_alloc(puc, filename, flags, int_cb);
    if (ret < 0)
        return ret;
    if (parent)
        av_opt_copy(*puc, parent);
    if (options &&
        (ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;
    if (options && (*puc)->prot->priv_data_class &&
        (ret = av_opt_set_dict((*puc)->priv_data, options)) < 0)
        goto fail;

    if (!options)
        options = &tmp_opts;

    av_assert0(!whitelist ||
               !(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
               !strcmp(whitelist, e->value));
    av_assert0(!blacklist ||
               !(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
               !strcmp(blacklist, e->value));

    if ((ret = av_dict_set(options, "protocol_whitelist", whitelist, 0)) < 0)
        goto fail;

    if ((ret = av_dict_set(options, "protocol_blacklist", blacklist, 0)) < 0)
        goto fail;

    if ((ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;

    ret = ffurl_connect(*puc, options);

    if (!ret)
        return 0;
fail:
    ffurl_closep(puc);
    return ret;
}

ffurl_alloc

先不分析,报错不在这里

ffurl_connect

内部对当前的prot->nam做黑白名单protocol_whitelist、protocol_blacklist做了一些校验;之后再调用url_open2或者url_open方法;

int ffurl_connect(URLContext *uc, AVDictionary **options)
{
    int err;
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;

    if (!options)
        options = &tmp_opts;

    // Check that URLContext was initialized correctly and lists are matching if set
    av_assert0(!(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
               (uc->protocol_whitelist && !strcmp(uc->protocol_whitelist, e->value)));
    av_assert0(!(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
               (uc->protocol_blacklist && !strcmp(uc->protocol_blacklist, e->value)));

    if (uc->protocol_whitelist && av_match_list(uc->prot->name, uc->protocol_whitelist, ',') <= 0) {
        av_log(uc, AV_LOG_ERROR, "Protocol '%s' not on whitelist '%s'!\n", uc->prot->name, uc->protocol_whitelist);
        return AVERROR(EINVAL);
    }

    if (uc->protocol_blacklist && av_match_list(uc->prot->name, uc->protocol_blacklist, ',') > 0) {
        av_log(uc, AV_LOG_ERROR, "Protocol '%s' on blacklist '%s'!\n", uc->prot->name, uc->protocol_blacklist);
        return AVERROR(EINVAL);
    }

    if (!uc->protocol_whitelist && uc->prot->default_whitelist) {
        av_log(uc, AV_LOG_DEBUG, "Setting default whitelist '%s'\n", uc->prot->default_whitelist);
        uc->protocol_whitelist = av_strdup(uc->prot->default_whitelist);
        if (!uc->protocol_whitelist) {
            return AVERROR(ENOMEM);
        }
    } else if (!uc->protocol_whitelist)
        av_log(uc, AV_LOG_DEBUG, "No default whitelist set\n"); // This should be an error once all declare a default whitelist

    if ((err = av_dict_set(options, "protocol_whitelist", uc->protocol_whitelist, 0)) < 0)
        return err;
    if ((err = av_dict_set(options, "protocol_blacklist", uc->protocol_blacklist, 0)) < 0)
        return err;

    err =
        uc->prot->url_open2 ? uc->prot->url_open2(uc,
                                                  uc->filename,
                                                  uc->flags,
                                                  options) :
        uc->prot->url_open(uc, uc->filename, uc->flags);

    av_dict_set(options, "protocol_whitelist", NULL, 0);
    av_dict_set(options, "protocol_blacklist", NULL, 0);

    if (err)
        return err;
    uc->is_connected = 1;
    /* We must be careful here as ffurl_seek() could be slow,
     * for example for http */
    if ((uc->flags & AVIO_FLAG_WRITE) || !strcmp(uc->prot->name, "file"))
        if (!uc->is_streamed && ffurl_seek(uc, 0, SEEK_SET) < 0)
            uc->is_streamed = 1;
    return 0;
}

尝试设置protocol_whitelist参数;

AVDictionary *opt = NULL;
av_dict_set(&opt, "protocol_whitelist", "rtmp", 0);
int ret = avio_open2(&outFormat->pb, streamAddress.c_str(), AVIO_FLAG_WRITE, NULL, &opt);

根据打印提示,通过搜索not on whitelist,可以发现此处返回的报错主要就是prot-name即协议名称不在白名单内;

我们只设置了rtmp,因为我们传入的是rtmp推流地址,其实rtmp本质上是基于tcp的,ffurl_connect在判断连接的时候估计会根据协议继承关系走好几次;

所以这里,根据分隔符',',我就稍微加了几个;

加完之后不会爆not on whitelist的错误了,但是之前的问题还是没有解决,所以只能继续往下看了;
这个protocol_whitelist也可以不设,默认会把当前传入的url协议设置上去

url_open2

从URLProtocol的定义可以看出,其中包含了用于协议读写的函数指针。例如: url_open():打开协议。 url_read():读数据。 url_write():写数据。 url_close():关闭协议。

url_open()本身是URLProtocol的一个函数指针,这个地方根据不同的协议调用的url_open()具体实现函数也是不一样的,例如file协议的url_open()对应的是file_open(),而file_open()最终调用了_wsopen(),_sopen()(Windows下)或者open()(Linux下,类似于fopen())这样的系统中打开文件的API函数;而libRTMP的url_open()对应的是rtmp_open(),而rtmp_open()最终调用了libRTMP的API函数RTMP_Init(),RTMP_SetupURL(),RTMP_Connect() 以及RTMP_ConnectStream()。

URLProtocol结构体

typedef struct URLProtocol {
    const char *name;
    int     (*url_open)( URLContext *h, const char *url, int flags);
    /**
     * This callback is to be used by protocols which open further nested
     * protocols. options are then to be passed to ffurl_open()/ffurl_connect()
     * for those nested protocols.
     */
    int     (*url_open2)(URLContext *h, const char *url, int flags, AVDictionary **options);
    int     (*url_accept)(URLContext *s, URLContext **c);
    int     (*url_handshake)(URLContext *c);

    /**
     * Read data from the protocol.
     * If data is immediately available (even less than size), EOF is
     * reached or an error occurs (including EINTR), return immediately.
     * Otherwise:
     * In non-blocking mode, return AVERROR(EAGAIN) immediately.
     * In blocking mode, wait for data/EOF/error with a short timeout (0.1s),
     * and return AVERROR(EAGAIN) on timeout.
     * Checking interrupt_callback, looping on EINTR and EAGAIN and until
     * enough data has been read is left to the calling function; see
     * retry_transfer_wrapper in avio.c.
     */
    int     (*url_read)( URLContext *h, unsigned char *buf, int size);
    int     (*url_write)(URLContext *h, const unsigned char *buf, int size);
    int64_t (*url_seek)( URLContext *h, int64_t pos, int whence);
    int     (*url_close)(URLContext *h);
    int (*url_read_pause)(URLContext *h, int pause);
    int64_t (*url_read_seek)(URLContext *h, int stream_index,
                             int64_t timestamp, int flags);
    int (*url_get_file_handle)(URLContext *h);
    int (*url_get_multi_file_handle)(URLContext *h, int **handles,
                                     int *numhandles);
    int (*url_get_short_seek)(URLContext *h);
    int (*url_shutdown)(URLContext *h, int flags);
    int priv_data_size;
    const AVClass *priv_data_class;
    int flags;
    int (*url_check)(URLContext *h, int mask);
    int (*url_open_dir)(URLContext *h);
    int (*url_read_dir)(URLContext *h, AVIODirEntry **next);
    int (*url_close_dir)(URLContext *h);
    int (*url_delete)(URLContext *h);
    int (*url_move)(URLContext *h_src, URLContext *h_dst);
    const char *default_whitelist;
} URLProtocol;

rtmp_open

之前有说过avio_check内部有调用prot协议结构体里的url_check,这里通过控制台也可以发现url_check为null,所以也进一步验证了此处avio_check == ffurl_open_whitelist;

image

librtmp.c

rtmpproto.c

根据结构体名称url_open2可以推测,rtmp_open最终应该执行的是rtmpproto.c里面函数;较之librtmp.c里的rtmp_open,二者的区别就在于rtmpproto的rtmp_open多了一个opts参数,所以我们最终研究的核心还是这个opts参数;

这边也可以看出,rtmp_open内部会执行ffurl_open_whitelist操作,之前也有提过,ffurl_open_whitelist会执行ffurl_connect函数,ffurl_connect又会执行url_open2函数,而url_open2本质上又是rtmp_open,即

ffurl_open_whitelist -> ffurl_connect -> url_open2(rtmp_open) -> ffurl_open_whitelist;
那这样岂不是形成了死循环🤔,想来ffmpeg也不会这么蠢,中间肯定有做什么操作;
分析发现,在rtmp_open的时候,会设置一些参数来跳出递归,具体传入什么值,这个也不是今天关注的重点;

之前也提到,当我们没传入opts时,也会报野指针的错误,通过简单阅读汇编代码,以及查看源码可以发现,内部其实是有给options赋值的;

由于看不到执行过程的源码,所以只能在执行av_dict_free之前下个断点,再通过控制台输入si,汇编单步调试,进入av_dict_free内部查看;

image

通过单步调试,发现这个free其实没问题,一开始关注的方向就有问题,所以只能继续看下面的方法,由于后面方法是之前走完的,所以当rtmp_close执行完之后,就崩溃了,现在只能把方向定位在rtmp_close了;

这个0x000000016d596798就是寄存器X0保存的地址,因为av_dict_free只有一个参数,所以X0中保存的就是我们的opts,刚开始打印发现,它是空的,所以这里没问题也是正常;

rtmp_close

继续往下看,一步一步走,最后找到崩溃的这行代码;

image

现在遇到一个比较关键的问题,_platform_strlen这玩意是啥。。。

根据直觉应该是strlen获取字符串长度的,前面的platform应该是链接器根据不同平台序列化的符号标志位,所以可以查看一下rtmp_close的源码;

static int rtmp_close(URLContext *h)
{
    RTMPContext *rt = h->priv_data;
    int ret = 0, i, j;

    if (!rt->is_input) {
        rt->flv_data = NULL;
        if (rt->out_pkt.size)
            ff_rtmp_packet_destroy(&rt->out_pkt);
        if (rt->state > STATE_FCPUBLISH)
            ret = gen_fcunpublish_stream(h, rt);
    }
    if (rt->state > STATE_HANDSHAKED)
        ret = gen_delete_stream(h, rt);
    for (i = 0; i < 2; i++) {
        for (j = 0; j < rt->nb_prev_pkt[i]; j++)
            ff_rtmp_packet_destroy(&rt->prev_pkt[i][j]);
        av_freep(&rt->prev_pkt[i]);
    }

    free_tracked_methods(rt);
    av_freep(&rt->flv_data);
    ffurl_closep(&rt->stream);
    return ret;
}

rtmp_close里面没有发现类似代码,但是它里面调用的方法有可以看到gen_fcunpublish_stream以及子函数ff_amf_write_string内部都有调用strlen,但是ff_amf_write_string传入的是固定值,所以这边就不看它;

static int gen_fcunpublish_stream(URLContext *s, RTMPContext *rt)
{
    RTMPPacket pkt;
    uint8_t *p;
    int ret;

    if ((ret = ff_rtmp_packet_create(&pkt, RTMP_SYSTEM_CHANNEL, RTMP_PT_INVOKE,
                                     0, 27 + strlen(rt->playpath))) < 0)
        return ret;

    av_log(s, AV_LOG_DEBUG, "UnPublishing stream...\n");
    p = pkt.data;
    ff_amf_write_string(&p, "FCUnpublish");
    ff_amf_write_number(&p, ++rt->nb_invokes);
    ff_amf_write_null(&p);
    ff_amf_write_string(&p, rt->playpath);

    return rtmp_send_packet(rt, &pkt, 0);
}

void ff_amf_write_string(uint8_t **dst, const char *str)
{
    bytestream_put_byte(dst, AMF_DATA_TYPE_STRING);
    bytestream_put_be16(dst, strlen(str));
    bytestream_put_buffer(dst, str, strlen(str));
}

所以这个时候,我就尝试着给gen_fcunpublish_stream打断点,看看能不能进入函数内部查看;这个方法看字面意思应该是 gen FCPublishing stream (for output),结束推送流;

很遗憾的是,gen_fcunpublish_stream方法没有被我们捕获到,可能当前rt->state 并没有大于STATE_FCPUBLISH吧,只能通过捕获它的上级rtmp_close继续查看了;可喜的是,这边看到了执行strlen关键函数ff_rtmp_packet_create,所以尝试进入内部查看它的执行过程;

image

但是在执行ff_rtmp_packet_create前,这一行代码也是strlen执行完就崩溃;bl指令表示跳到0x110b74e78这个地方去执行,si进去发现0x110b74e78地址就是指向strlen函数;

可见,在ff_rtmp_packet_destroy跟gen_delete_stream代码执行过程中,执行了strlen,具体啥时候调用的就不深究了,看看当前strlen传入的值吧;

image

把x1跟x0按位与之后,x1保存的地址也被清空了,q0指向的是野指针,ldr把x1中的空地址放入q0,崩了;

image

解决思路 - 捕获闪退方法:

上面分析的很多,其实问题一直很明确的指向strlen,那么我就想着看看能不能hook strlen方法,看看闪退能不能定位到这里来;

结果很喜人,内部闪退问题成功定位到自定义方法,所以这里对尝试对strlen修改一下,崩溃问题就得以改善了,最后附上完整hook代码;

- (void)hookStrlen { 
    rebind_symbols((struct rebinding[1]){
      {
        "strlen",
          (void *)chrisstrlen,
          (void **)&sys_strlen
      }}, 1);

    //    strlen("ddddd");
}

static size_t(* sys_strlen)(const char *str);

size_t chrisstrlen(const char *str) {
//    printf("chrisstrlen=====成功捕获到strlen\n");
    return str ? sys_strlen(str) : 0;
}

总结:其实一开始,我们就知道问题出现在rtmp_open,看前文中的这张图,我只关注rtmp_open,一度把问题定位在avio_open2的最后options参数,还反复去查看了obs的源码,导致走了很多的弯路,但是其实真正崩溃的一直被我忽略的最后一步堆栈指向_platform_strlen,直到扒到rtmp_close才去关注这个地方,虽然走了一些弯路,但是想来也是不错,至少通过这一通分析,我基本了解了avio_open2的大体实现思路,也对一些汇编指令及lldb调试方式有了更多认识;


其他:在排查这个问题的时候,一度因为自己写的代码调不到导入ffmpeg源码里面的函数,屡屡尝试,导致气血攻心,差点晕厥,但是现在回头想想,人之所以会糊涂就是因为容易上头,有时候不一定非得在一颗树上吊死,毕竟条条大路通罗马,退一步可能会发现更大的世界;

ffio_fdopen

打开io操作,跟当前涉及问题无关,暂时还没细看

附录

obs 源码解析笔记_年轻的古尔丹的博客-CSDN博客_obs rtp
FFmpeg源代码简单分析:avio_open2()_雷霄骅的博客-CSDN博客
iOS逆向 HOOK原理之fishhook
fishhook github

部分汇编指令

and:按位与
adrp:立即数,作用是把pc寄存器跟立即数按照一定规则计算后赋值给寄存器.
ldr:LDR R1,[R0] 是读取R0地址所对应的数据给R1寄存器

LDR R0,[R1] ;R1中代表存储器地址,在存储器中将R1地址处的数据加载到寄存器R0中。

LDR R0,=0x00000040 ;将立即数装入R0中,如果立即数小,该指令等效 MOV R0,#64,如果立即数很大比如占据32bit,那么该指令将变成伪指令,见下条。

LDR R0,=0xF0000000 ;立即数很大,无法将立即数和指令合并成32bit,指令会被编译器拆分为LDR R0, [PC, #offset]; .word 0xF00000000两条指令,即先将立即数利用.word指令存储在该LDR指令附近,编译器计算立即数与当前正在执行指令PC(program counter)指针的偏差offset,注意ARM是流水线指令,采用取指令,译指令和执行指令。

LDR R0,label;将label标号所代表的寄存器地址中数据装在到R0。

str:STR R1,[R0] 是将R1里的数据给到R0地址中,跟ldr相反,一个读,一个存
br:BR即BRANCH,分支,无条件跳转到芯片支持的所有地址范围

最后附上一张雷神blog下摘下来的结构图,当然,这是多年之前版本的结果图了,现在ffurl_open已经被ffurl_open_whitelist取代了;

image

下面是基于ffurl_open我自己摘抄出来调用的一些方法:

int ffurl_connect(URLContext *uc, AVDictionary **options)
{
    int err =
        uc->prot->url_open2 ? uc->prot->url_open2(uc,
                                                  uc->filename,
                                                  uc->flags,
                                                  options) :
        uc->prot->url_open(uc, uc->filename, uc->flags);

    if (err)
        return err;
    uc->is_connected = 1;
    /* We must be careful here as ffurl_seek() could be slow,
     * for example for http */
    if ((uc->flags & AVIO_FLAG_WRITE) || !strcmp(uc->prot->name, "file"))
        if (!uc->is_streamed && ffurl_seek(uc, 0, SEEK_SET) < 0)
            uc->is_streamed = 1;
    return 0;
}

int ffurl_open(URLContext **puc, const char *filename, int flags,
               const AVIOInterruptCB *int_cb, AVDictionary **options)
{
    int ret = ffurl_alloc(puc, filename, flags, int_cb);
    if (ret < 0)
        return ret;
    if (options && (*puc)->prot->priv_data_class &&
        (ret = av_opt_set_dict((*puc)->priv_data, options)) < 0)
        goto fail;
    if ((ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;
    ret = ffurl_connect(*puc, options);
    if (!ret)
        return 0;
fail:
    ffurl_close(*puc);
    *puc = NULL;
    return ret;
}

int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,
                        const AVIOInterruptCB *int_cb, AVDictionary **options,
                        const char *whitelist, const char *blacklist
                       )
{
    URLContext *h;
    int err;

    *s = NULL;

    err = ffurl_open(&h, filename, flags, int_cb, options);
    if (err < 0)
        return err;
//    err = ffio_fdopen(s, h);
    if (err < 0) {
        ffurl_close(h);
        return err;
    }
    return 0;
}

int avio_open22(AVIOContext **s, const char *filename, int flags,
               const AVIOInterruptCB *int_cb, AVDictionary **options)
{
    return ffio_open_whitelist(s, filename, flags, int_cb, options, NULL, NULL);
}

相关文章

网友评论

      本文标题:avio_open2崩溃分析

      本文链接:https://www.haomeiwen.com/subject/pxsefdtx.html