文章

libyuv 转奇数行 NV12 到 RGB 毕现 EXC_BAD_ACCESS 崩溃

libyuv 转奇数行 NV12 到 RGB 毕现 EXC_BAD_ACCESS 崩溃

背景

记录一次看似由 SIMD 优化代码和共享内存读取保护冲突引发的崩溃,实际是 Apple 和 Google 在特殊情况下数据处理不同导致崩溃的经典案例。

问题现场:在使用 libyuv 将摄像头输出的 NV12 格式的 CVPixelbuffer 转成 BGRA 格式时,仅当 CVPixelBuffer 高度为奇数时会引发 NEON 内存越界崩溃,堆栈如下:

1
2
3
4
5
stop reason = EXC_BAD_ACCESS (code=1, address=0x141134000)
frame #0: 0x00000001031ff000 MetalStudio`NV12ToARGBRow_NEON + 32
frame #1: 0x00000001031f1b80 MetalStudio`NV12ToARGBRow_Any_NEON + 136
frame #2: 0x00000001031e505c MetalStudio`NV12ToARGBMatrix + 236
frame #3: 0x00000001031d12ec MetalStudio`my_NV12ToARGBMatrix

之前的项目也用过,但从来没有遇到在转换过程中崩溃的情况。libyuv 之所以转换效率很高是因为使用了 ARM 架构 的 SIMD 技术,通过堆栈也很直观的发现崩溃到了 NEON 相关函数上。

崩溃堆栈导致的误会

由于崩溃发生在 libyuv 库的函数里,加上当前 libyuv 库的代码是 21 年的老代码,所以怀疑是对 Apple M 芯片适配不好存在兼容性问题。于是将源码更新到 main 分支最新提交,但在编译时发现 libyuv 的库进行了拆分,由一个库拆分成了 4 个库,编译时先逐个编译,最后合并成一个 libyuv.a 时命令执行成功了,就是找不到产物,折腾半天后通过二分法找到了 24 年初的代码还没有分成 4 个库编译,于是先用这个相对较新的 libyuv 版本。

经过测试没有任何好转,还是崩溃,堆栈还是一模一样。接下来询问 AI 如何解决,AI 的焦点也在 NEON 上,AI 给出的答案是数据需要做对齐的,甚至数据起始地址都有讲究,折腾一番仍旧无法解决。

malloc 闪亮登场

在调试过程中,发现了一个奇特的现象,当我将 CVPixelBuffer 中获取的 UV 平面数据,拷贝到一个由 malloc 分配的新缓冲区 (src_uv2),并将新指针传入转换函数时,崩溃立即消失。

1
2
3
4
5
6
7
8
// 原始的崩溃代码(或其逻辑):
// int result = NV12ToARGBMatrix(src_y, ..., src_uv, ...); 

// 修复崩溃的测试代码:
uint8_t*src_uv2 = (uint8_t*)malloc(kSizeUV);
memcpy(src_uv2, src_uv, kSizeUV);
int result = NV12ToARGBMatrix(src_y, ..., src_uv2, ...);
free(src_uv2); 

既然发生了 EXC_BAD_ACCESS 崩溃,那肯定就是内存上出了问题了,没有办法,那就试着计算一遍内存地址试试吧。

定位问题

结合崩溃地址进行了精确计算,打印 CVPixelBuffer 图像信息如下:

平面 (Plane)宽度 (Width)高度 (Height)步长 (bytesPerRow)
0 (Y)11886151280
1 (UV)5943071280

计算崩溃的内存地址:$0x141134000 - 0x1410d4000 = 393216 字节,CVPixelBuffer 内存最大边界是:1280x307=392960 字节,可看出 libyuv 访问的地址的确超出了 CVPixelBuffer 申请的内存空间。反推这个地址位于第 308 行,由于 CVPixelBuffer 的内存不是在主存上申请的,而是一块特殊的CPU和GPU共享的区域上申请的,它的边界感很强,一旦 NEON 优化代码尝试读取超出分配大小,就会触发操作系统的内存保护机制。

对于 malloc 而言,内存是在主存上申请的,我测试了下申请 307 行的内存不会发生崩溃,malloc 分配的内存后面是堆的空闲区域,这些区域对进程是可读的。当 libyuv 越界读取时,它读取到了垃圾数据,但程序不会因此崩溃。这让我想起来大学学习 C 语言时老师就曾讲过,C 语言没有内存越界检查机制,只有当访问了不属于当前进程的内存地址时才会崩溃。

解决问题

当遇到奇数高的图像时,CVPixelBuffer 提供的 uv 行数是 y_height/2,但是 libyuv 按照 uv 行数 y_height/2 + 1 处理的,所以数据源就少了一行。

回顾之前我没遇到过崩溃是因为没有处理过奇数高度的画面,我修改了 libyuv 来解决这个问题,这样执行转换时效率最高。原理是处理到最后一行时如果 uv 不够了,那么 uv 的指针就不要下移了,使用上次的 uv 即可。这样处理的好处是无需复制 CVPixelBuffer,还能继续使用高效的 NEON 的优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*
convert nv12 to argb with matrix,uv height safe
because cvpixelbuffer uv height is y_height/2
libyuv need uv height is y_height/2 + 1
for example:
y height is 615
uv height is 307
libyuv need uv height is 308
*/
LIBYUV_API
int NV12ToARGBMatrixSafely(const uint8_t* src_y,
                     int src_stride_y,
                     const uint8_t* src_uv,
                     int src_stride_uv,
                     uint8_t* dst_argb,
                     int dst_stride_argb,
                     const struct YuvConstants* yuvconstants,
                     int width,
                     int height,
                    int uv_height) {
  int y;
  void (*NV12ToARGBRow)(
      const uint8_t* y_buf, const uint8_t* uv_buf, uint8_t* rgb_buf,
      const struct YuvConstants* yuvconstants, int width) = NV12ToARGBRow_C;
  assert(yuvconstants);
  if (!src_y || !src_uv || !dst_argb || width <= 0 || height == 0) {
    return -1;
  }
  // Negative height means invert the image.
  if (height < 0) {
    height = -height;
    dst_argb = dst_argb + (height - 1) * dst_stride_argb;
    dst_stride_argb = -dst_stride_argb;
  }
#if defined(HAS_NV12TOARGBROW_SSSE3)
  if (TestCpuFlag(kCpuHasSSSE3)) {
    NV12ToARGBRow = NV12ToARGBRow_Any_SSSE3;
    if (IS_ALIGNED(width, 8)) {
      NV12ToARGBRow = NV12ToARGBRow_SSSE3;
    }
  }
#endif
#if defined(HAS_NV12TOARGBROW_AVX2)
  if (TestCpuFlag(kCpuHasAVX2)) {
    NV12ToARGBRow = NV12ToARGBRow_Any_AVX2;
    if (IS_ALIGNED(width, 16)) {
      NV12ToARGBRow = NV12ToARGBRow_AVX2;
    }
  }
#endif
#if defined(HAS_NV12TOARGBROW_NEON)
  if (TestCpuFlag(kCpuHasNEON)) {
    NV12ToARGBRow = NV12ToARGBRow_Any_NEON;
    if (IS_ALIGNED(width, 8)) {
      NV12ToARGBRow = NV12ToARGBRow_NEON;
    }
  }
#endif
#if defined(HAS_NV12TOARGBROW_MSA)
  if (TestCpuFlag(kCpuHasMSA)) {
    NV12ToARGBRow = NV12ToARGBRow_Any_MSA;
    if (IS_ALIGNED(width, 8)) {
      NV12ToARGBRow = NV12ToARGBRow_MSA;
    }
  }
#endif
#if defined(HAS_NV12TOARGBROW_LSX)
  if (TestCpuFlag(kCpuHasLSX)) {
    NV12ToARGBRow = NV12ToARGBRow_Any_LSX;
    if (IS_ALIGNED(width, 8)) {
      NV12ToARGBRow = NV12ToARGBRow_LSX;
    }
  }
#endif
#if defined(HAS_NV12TOARGBROW_LASX)
  if (TestCpuFlag(kCpuHasLASX)) {
    NV12ToARGBRow = NV12ToARGBRow_Any_LASX;
    if (IS_ALIGNED(width, 16)) {
      NV12ToARGBRow = NV12ToARGBRow_LASX;
    }
  }
#endif
#if defined(HAS_NV12TOARGBROW_RVV)
  if (TestCpuFlag(kCpuHasRVV)) {
    NV12ToARGBRow = NV12ToARGBRow_RVV;
  }
#endif

  //because src_uv will add src_stride_uv,so after added src_uv should not exceed uv_height
  uv_height--;

  for (y = 0; y < height; ++y) {
    NV12ToARGBRow(src_y, src_uv, dst_argb, yuvconstants, width);
    dst_argb += dst_stride_argb;
    src_y += src_stride_y;
    //cvpixelbuffer may have different uv height: height >> 1 - 1
    if (y & 1 && (y >> 1) < uv_height) {
      src_uv += src_stride_uv;
    }
  }
  return 0;
}
本文由作者按照 CC BY 4.0 进行授权

© debugly. 保留部分权利。

本站采用 Jekyll 主题 Chirpy