В первой части я рассказал о кодировании видео в Linux с использованием Nvidia NVENC. Как уже упоминалось ранее, Nvidia для десктопных видеокарт ограничивает количество потоков кодирования до двух сессий на систему. Данная часть посвящена борьбе с этим ограничением.

Окружение


Всё описываемое происходит на машине с GTX 970 и установленным FFmpeg, в соответствии с конфигурацией, рассмотренной ранее.

Внешние проявления


При попытке запустить параллельно кодирование более двух потоков видео FFmpeg выдаст ошибку:
...
[nvenc @ 0x3187200] OpenEncodeSessionEx failed: 0xa - invalid license key?
...
Error while opening encoder for output stream #0:0 - maybe incorrect parameters such as bit_rate, rate, width or height

Чтобы постоянно и удобно воспроизводить эту ошибку, я запустил два раза перекодирование в ffmpeg и поставил процесс на паузу, послав ему SIGSTOP (Ctrl+Z в терминале):
$ /usr/local/bin/ffmpeg -y -i input.mov -vcodec nvenc output1.mp4
...
Stream mapping:
  Stream #0:1 -> #0:0 (mpeg4 (native) -> h264 (nvenc))
  Stream #0:0 -> #0:1 (aac (native) -> aac (libfdk_aac))
Press [q] to stop, [?] for help
frame=   81 fps= 80 q=0.0 size=    1362kB time=00:00:03.24 bitrate=3444.9kbits/s
[1]+  Stopped                 /usr/local/bin/ffmpeg -y -i input.mov -vcodec nvenc out1.mp4
$ /usr/local/bin/ffmpeg -y -i input.mov -vcodec nvenc output2.mp4
...
Stream mapping:
  Stream #0:1 -> #0:0 (mpeg4 (native) -> h264 (nvenc))
  Stream #0:0 -> #0:1 (aac (native) -> aac (libfdk_aac))
Press [q] to stop, [?] for help
frame=   81 fps= 80 q=0.0 size=    1362kB time=00:00:03.24 bitrate=3444.9kbits/s
[2]+  Stopped                 /usr/local/bin/ffmpeg -y -i input.mov -vcodec nvenc out1.mp4


ltrace


Посмотрим на это место детальнее:
$ ltrace /usr/local/bin/ffmpeg -y -i input.mov -vcodec nvenc out3.mp4 2>&1 | less
...
dlsym(0x313e360, "cuInit")                       = 0x7f93974182c0
dlsym(0x313e360, "cuDeviceGetCount")             = 0x7f9397418760
dlsym(0x313e360, "cuDeviceGet")                  = 0x7f93974185c0
dlsym(0x313e360, "cuDeviceGetName")              = 0x7f93974188e0
dlsym(0x313e360, "cuDeviceComputeCapability")    = 0x7f9397418f80
dlsym(0x313e360, "cuCtxCreate_v2")               = 0x7f9397419940
dlsym(0x313e360, "cuCtxPopCurrent_v2")           = 0x7f9397419df0
dlsym(0x313e360, "cuCtxDestroy_v2")              = 0x7f9397419af0
dlopen("libnvidia-encode.so.1", 1)               = 0x3231970
dlsym(0x3231970, "NvEncodeAPICreateInstance")    = 0x7f93970d4370
posix_memalign(0x7fffb429d490, 32, 640, 0x7fffb429d3f8) = 0
memset(0x3141420, '\0', 640)                     = 0x3141420
free(0)                                          = <void>
pthread_mutex_lock(0x19a90e0, 8, 0xf8f340, 0x7fffb429d3f8) = 0
__vsnprintf_chk(0x7fffb429c3b4, 1004, 1, -1)     = 20
__vsnprintf_chk(0x7fffb429cbb4, 1004, 1, -1)     = 55
__snprintf_chk(0x7fffb429cfa0, 1024, 1, 1024)    = 75
strcmp("[nvenc @ 0x3187200] OpenEncodeSe"..., "\n") = 81
__strcpy_chk(0x19a8cc0, 0x7fffb429cfa0, 1024, 0) = 0x19a8cc0
fputs("[nvenc @ 0x3187200] ", 0x7f93a554e1c0[nvenc @ 0x3187200] )    = 1
fputs("OpenEncodeSessionEx failed: 0xa "..., 0x7f93a554e1c0OpenEncodeSessionEx failed: 0xa - invalid license key?
) = 1
pthread_mutex_unlock(0x19a90e0, 0, 0x7fffb429cbb4, -1) = 0
...

Видно, что какая-то ошибка вывелась, но что вызывалось не видно из-за динамической загрузки библиотеки и её символов (функций).

Исходный код FFmpeg


Поищем это место в исходниках самого FFmpeg, чтобы взять его отправной точкой.
~/ffmpeg-2.7.1$ fgrep -r OpenEncodeSessionEx
...
libavcodec/nvenc.c:606:    nv_status = p_nvenc->nvEncOpenEncodeSessionEx(&encode_session_params, &ctx->nvencoder);
libavcodec/nvenc.c:609:        av_log(avctx, AV_LOG_FATAL, "OpenEncodeSessionEx failed: 0x%x - invalid license key?\n", (int)nv_status);
...

Всё понятно, тут и поставим брейкпоинт.

Светлый GDB


GNU Debugger — это основной юниксовый отладчик, предназначение которого отлаживать программы, дабы они не генерировали ошибки.

Для ориентирования в машинном коде скомпилированного приложения и соотношения его с исходным кодом, желательно чтобы приложение было скомпилировано с отладочными символами. Они, в первую очередь, содержат информацию о соответствии машинного и исходных кодов.

В большинстве дистритубивов пакеты содержат бинарные файлы с обрезанной отладочной информацией и для некоторых пакетов отладочная информация поставляется в виде отдельного пакета. В ubuntu это, как правило, пакеты с суффиксом -dbg. В centos нужно подключить репозиторий с отладочными символами и воспользоваться утилитой debuginfo-install из состава yum-utils, которая установит отладочные символы для пакета и его зависимостей.

В нашем же случае с самосборным FFmpeg, необрезанный бинарь доступен в его сборочной директории под именем ffmpeg_g. Мы можем запустить его под отладчиком и сразу поставить брейкпоинт на нужную строчку в исходном коде.
# gdb ffmpeg-2.7.1/ffmpeg_g
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
...
Reading symbols from ffmpeg-2.7.1/ffmpeg_g...done.
(gdb)

Установим брейкпоинт на интересующее нас место:
(gdb) break nvenc.c:606
Breakpoint 1 at 0x44a890: file libavcodec/nvenc.c, line 606.

Запускаем программу, указав аргументы запуска через аргументы команде run:
(gdb) run -i in.mov -vcodec nvenc out3.mp4
...
Breakpoint 1, nvenc_encode_init (avctx=0x1b806e0) at libavcodec/nvenc.c:606
606         nv_status = p_nvenc->nvEncOpenEncodeSessionEx(&encode_session_params, &ctx->nvencoder);
(gdb) list
601         }
602
603         encode_session_params.device = ctx->cu_context;
604         encode_session_params.deviceType = NV_ENC_DEVICE_TYPE_CUDA;
605
606         nv_status = p_nvenc->nvEncOpenEncodeSessionEx(&encode_session_params, &ctx->nvencoder);
607         if (nv_status != NV_ENC_SUCCESS) {
608             ctx->nvencoder = NULL;
609             av_log(avctx, AV_LOG_FATAL, "OpenEncodeSessionEx failed: 0x%x - invalid license key?\n", (int)nv_status);
610             res = AVERROR_EXTERNAL;
(gdb) 

Брейкпоинт сработал и мы действительно достигли нужного места в коде. Для удобства нажатием Ctrl+X, Ctrl+A можно переключить GDB в режим разделения командного экрана с экраном исходника.

Пройдём код пошагово до возврата этой функции.
606         nv_status = p_nvenc->nvEncOpenEncodeSessionEx(&encode_session_params, &ctx->nvencoder);
(gdb) step
603         encode_session_params.device = ctx->cu_context;
(gdb) step
606         nv_status = p_nvenc->nvEncOpenEncodeSessionEx(&encode_session_params, &ctx->nvencoder);
(gdb) step
607         if (nv_status != NV_ENC_SUCCESS) {

Последнюю введённую команду, к слову, можно повторять простым нажатием Enter. Возврат функции сохраняется в локальную переменную nv_status. Посмотрим, что же в ней:
(gdb) info locals
...
nv_status = NV_ENC_ERR_OUT_OF_MEMORY
...

Убиваем висящие ffmpeg-и, в отладчике перезапускаем программу командой run. Это запустит её с теми же аргументами. Дойдя до того же места, мы увидим:
(gdb) info locals
...
nv_status = NV_ENC_SUCCESS
...

Таким образом, функция создания сессии кодирования возвращает NV_ENC_SUCCESS (0) в случае успеха, либо NV_ENC_ERR_OUT_OF_MEMORY (10), если пользователь уже открыл 2 сессии кодирования. Спустимся вглубь этой функции.

Тёмный GDB


Дойдём до вызова этой функции и спустимся вглубь неё.
Breakpoint 1, nvenc_encode_init (avctx=0x1b806e0) at libavcodec/nvenc.c:606
606         nv_status = p_nvenc->nvEncOpenEncodeSessionEx(&encode_session_params, &ctx->nvencoder);
(gdb) layout asm

Интерфейс GDB примет вид:

Принудительно перерисовать экран, если он оказался замусорен, можно нажатием Ctrl+L.

Указатель исполнения стоит на загрузке параметров функции перед её вызовом. Отправляемся вглубь:
(gdb) set step-mode on
(gdb) step
...

Оказываемся внутри функции из /usr/lib/x86_64-linux-gnu/libnvidia-encode.so.1:
  >|0x7fffe289b010  mov    %rbp,-0x20(%rsp)                                                                                                                                       |
   |0x7fffe289b015  mov    %r12,-0x18(%rsp)                                                                                                                                       |
   |0x7fffe289b01a  mov    $0x6,%ebp                                                                                                                                              |
   |0x7fffe289b01f  mov    %rbx,-0x28(%rsp)                                                                                                                                       |
   |0x7fffe289b024  mov    %r13,-0x10(%rsp)                                                                                                                                       |
   |0x7fffe289b029  mov    %rsi,%r12                                                                                                                                              |
   |0x7fffe289b02c  mov    %r14,-0x8(%rsp)                                                                                                                                        |
   |0x7fffe289b031  sub    $0xa8,%rsp                                                                                                                                             |
   |0x7fffe289b038  test   %rdi,%rdi                                                                                                                                              |
   |0x7fffe289b03b  sete   %dl                                                                                                                                                    |
   |0x7fffe289b03e  test   %rsi,%rsi                                                                                                                                              |
   |0x7fffe289b041  sete   %al                                                                                                                                                    |
   |0x7fffe289b044  or     %al,%dl                                                                                                                                                |
   |0x7fffe289b046  jne    0x7fffe289b060                                                                                                                                         |
   |0x7fffe289b048  mov    (%rdi),%eax

Так проходим пошагово всю функцию, записывая на бумажке направления условных переходов. Заходя в вызовы других функций, чтобы не углубляться, сразу выходим из них командой finish. Проделываем это 2 раза, когда все кодирующие сессии заняты и когда есть свободные.

Следуя такой методологии, приходим к тому, что разветвление начинается с места:
   |0x7fffe289b319                                  callq  0x7fffe288d510                                                                                                         |
   |0x7fffe289b31e                                  test   %eax,%eax                                                                                                              |
   |0x7fffe289b320                                  mov    %eax,%ebp                                                                                                              |
   |0x7fffe289b322                                  jne    0x7fffe289b332                                                                                                         |


Функция, принимающая решение:
  >|0x7fffe288d510  mov    %rbx,-0x20(%rsp)                                                                                                                                       |
   |0x7fffe288d515  mov    %rbp,-0x18(%rsp)                                                                                                                                       |
   |0x7fffe288d51a  mov    %rdi,%rbx                                                                                                                                              |
   |0x7fffe288d51d  mov    %r12,-0x10(%rsp)                                                                                                                                       |
   |0x7fffe288d522  mov    %r13,-0x8(%rsp)                                                                                                                                        |
   |0x7fffe288d527  sub    $0x28,%rsp                                                                                                                                             |
   |0x7fffe288d52b  test   %rsi,%rsi                                                                                                                                              |
   |0x7fffe288d52e  mov    %rsi,%r12                                                                                                                                              |
   |0x7fffe288d531  mov    %rcx,%r13                                                                                                                                              |
   |0x7fffe288d534  mov    $0x6,%ebp                                                                                                                                              |
   |0x7fffe288d539  je     0x7fffe288d54d                                                                                                                                         |
   |0x7fffe288d53b  dec    %edx                                                                                                                                                   |
   |0x7fffe288d53d  mov    $0xa,%bpl                                                                                                                                              |
   |0x7fffe288d540  je     0x7fffe288d568                                                                                                                                         |
   |0x7fffe288d542  cmpb   $0x1,0x10(%rbx)                                                                                                                                        |
   |0x7fffe288d546  je     0x7fffe288d5a3                                                                                                                                         |
   |0x7fffe288d548  mov    $0x2,%ebp                                                                                                                                              |
   |0x7fffe288d54d  mov    %ebp,%eax                                                                                                                                              |
   |0x7fffe288d54f  mov    0x8(%rsp),%rbx                                                                                                                                         |
   |0x7fffe288d554  mov    0x10(%rsp),%rbp                                                                                                                                        |
   |0x7fffe288d559  mov    0x18(%rsp),%r12                                                                                                                                        |
   |0x7fffe288d55e  mov    0x20(%rsp),%r13                                                                                                                                        |
   |0x7fffe288d563  add    $0x28,%rsp                                                                                                                                             |
   |0x7fffe288d567  retq                                                                                                                                                          |

Эта функция что-то проверяет в памяти, если успешно, — то делает что-то и возвращает 0, если нет, — то 10. Ровно такой код ошибки из результата этой функции возвращает сама nvEncOpenEncodeSessionEx() в случае неудачи. Попробуем игнорировать возврат этой функции, как если бы она вернула 0.

Останавливаемся после callq 0x7fffe288d510 и перед test %eax,%eax. Обнуляем регистр с результатом функции и продолжаем свободное выполнение программы:
(gdb) set $eax = 0
(gdb) continue

Перекодировка началась! И даже производит правильные результаты. Значит необходимо, чтобы в коде всегда было так. Зафиксируем это в самой libnvidia-encode.so.1

Нужно понять где это место находится в физическом файле на диске. Узнаем какому смещению в файле соответствует виртуальный адрес в коде библиотеки, загруженной в память.
(gdb) info proc mappings
process 1692
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
...
      0x7fffe2887000     0x7fffe28a8000    0x21000        0x0 /usr/lib/x86_64-linux-gnu/libnvidia-encode.so.346.46
...

Нас интересует окрестность адреса 0x7fffe289b31e, она попадает в этот регион. Тогда смещение в файле равно: адрес — начальный адрес + смещение сегмента.
(gdb) print/x 0x7fffe289b31e - 0x7fffe2887000 + 0x0
$7 = 0x1431e


Biew


Осталось пропатчить сам файл. Я не нашёл пока ничего лучше, чем biew (был переименован в beye). Предварительно сделав резервную копию, исправим файл:
biew /usr/lib/x86_64-linux-gnu/libnvidia-encode.so.346.46

В нём: F2 -> Disassembler, F5 -> 1431e
Получим такую картину:


Выглядит точь-в-точь как искомый код, значит мы попали верно. Нам необходимо, чтобы в регистре eax оказался 0, а условный переход не произошёл никогда.

Нажатие F4 включает режим редактирования. В biew нет такого удобного режима как в hiew, в котором можно непосредственно вводить инструкцию, а редактор её ассемблирует. Поэтому придётся манипулировать опкодами численно. Записываем, к примеру, так:


Байт по смещению 0x1431e со значения 0x85 меняем на 0x29. Инструкция «test eax, eax» превращается в «sub eax, eax». Два байта по смещениям 0x14322 и 0x14323 заменяем на 0x90 — это широко известный опкод nop.

Итог


Полученное решение вполне хорошо работает. Применив стандартные инструменты, можно достичь многого и расширить границы возможного.

Комментарии (7)


  1. ValdikSS
    15.07.2015 00:01
    -5

    А покажите, пожалуйста, пример кодирования H.265, и с какой скоростью оно это делает. Очень интересно.
    И, кстати, надеюсь, что в конце сентября покажу уже сервис по распределенному кодированию видео, о концепции которого писал год назад. Сейчас, я считаю, самое время: людям нужно кодировать и в h.265, и в vp8, и в vp9, да еще и 4K-видео вот-вот пойдет потоком, а тот же ZenCoder — один из самых популярных сервисов по кодированию видео ­— возьмет с вас более $2 (!!!) за кодирование 3-минутного видеоролика в 4K в H.265. Мой же сервис будет не только позволять устанавливать все параметры кодирования для любого видеокодека, и использовать встроенные фильтры ffmpeg, но и будет заметно дешевле. Кодирование подобного видео обойдется примерно в 30 центов.


    1. YourChief Автор
      15.07.2015 02:00
      +4

      87 fps для 1080p

      Оригинал
      H.265


      1. stepik777
        15.07.2015 18:44

        Решил для сравнения закодировать то же видео с помощью x264 (на процессоре i7-3770K). Подобрал параметры, чтобы получилась такая же скорость кодирования и размер файла (preset=ultrafast, crf=24.5).

        Получившееся видео. При сравнении на глаз и там и там артефакты, какое видео лучше — непонятно.


        1. ValdikSS
          15.07.2015 21:21

          Но, согласитесь, довольно достойный результат для H.265 с такой скоростью. Я несколько удивлен даже.


  1. viktprog
    15.07.2015 01:11

    Хм, а может это не лучшая идея просто игнорировать возвращенное значение? Ведь функция все-таки зачем-то вызывается. Мне кажется, стоило проверить, что на самом деле не так…
    Кроме того, я не вижу, как функция явным образом меняет eax, да и код функции приведен не весь (куда ведет je 0x7fffe288d568?)


    1. YourChief Автор
      15.07.2015 01:53

      Код не полностью приведён, это правда, меняется ниже. Совпадение размера окна ассемлерного кода в отладчике и инструкции ret вводит в заблуждение.

      Правда в том, что eax меняется вообще в другом модуле.
      www.dropbox.com/s/9by7mkr9lh4ajg7/%D0%A1%D0%BA%D1%80%D0%B8%D0%BD%D1%88%D0%BE%D1%82%202015-07-15%2001.39.13.png?dl=0

      То есть, когда первая загадочная функция вызывается, она что-то делает, получает через dlsym указатель на другую библиотечную функцию и прыгает по нему. Не вызывает, а просто прыгает. Возврат на пропатченную мною инструкцию происходит уже из другого модуля. Мне кажется, в коде это выглядит как хвостовая оптимизация, когда функция всегда возвращает результат другой функции. Вызов опускается, заменяется на прыжок и управление возвращается уже вызвавшему первую функцию. Мне кажется, там ничего ценного, раз она безусловно возвращается и результат не проверяется.


  1. apangin
    15.07.2015 10:23
    -9

    Из условий лицензии:

    Licensee agrees not to disassemble, decompile or reverse engineer the Object Code