Как переопределить c ++ malloc / free в javascript (emscripten)? - PullRequest
1 голос
/ 08 мая 2019

Я переопределяю Module._malloc и Module._free в Javascript (emscripten), оборачивая оригинальную функцию и просто добавляя Console.log для отображения адреса памяти, размера и общего объема выделенной памяти.

Я обнаружил, чтоНовая функция только перехватывает вызовы Javascript для Module._malloc и Module._free, и не перехватывает вызовы уровня c ++ для malloc () и free ().Я хотел бы знать, почему.

Основываясь на ответе г-на Офрии здесь https://stackoverflow.com/a/34057348/4806940, Module._malloc и Module._free - преобразованный эквивалентный код функций malloc () и free () из c ++.

Я использую emscripten 1.35.0

Редактировать: Вот как я обернул функцию в javascript

var _defaultMalloc = Module._malloc;
var _defaultFree = Module._free;

var _totalMemoryUsed = 0;
var _mallocTracker = {};
Module._malloc = function(size) {
   _totalMemoryUsed += size;
   var ptr = _defaultMalloc(size)
   _mallocTracker[ptr] = size;

   console.log("MALLOC'd @" + ptr + " " + size + " bytes -- TOTAL USED " + _totalMemoryUsed + " bytes");
   return ptr;
}

Module._free = function(ptr) {
   var size = _mallocTracker[ptr];
   _totalMemoryUsed -= size;

   console.log("FREE'd @" + ptr + " " + size + " bytes -- TOTAL USED " + _totalMemoryUsed + " bytes");
   return _defaultFree(ptr);
}

1 Ответ

2 голосов
/ 08 мая 2019

Краткий ответ : ваша попытка обернуть malloc / free не работает, потому что объект Module, который предоставляет реализацию Emscripten malloc() / free() являются не точками входа, вызываемыми собственным кодом C ++.Однако, немного взломав, есть способы, которыми вы можете отследить эти вызовы.


Почему ваши переопределения не работают

Я думаю, что ответВы можете процитировать более точную формулировку: эмуляция вызовов C ++ malloc() и free() предоставляется в Module._malloc() и Module._free(), но это не записьточки, вызываемые преобразованным кодом C ++.

Примечание : я обычно буду говорить только о malloc до конца этого ответа, но по существу все, что относится к malloc, также относится к free.

Я оставлю все ужасные подробности того, как Emscripten обрабатывает malloc(), позже, но вкратце:

  • Используя "стандартные настройки", Emscriptenкомпилирует программу на C ++ в a.out.js.

  • Большой фрагмент этого файла создает объект asm.Он содержит весь преобразованный код C ++ (например, реализацию JavaScript _main()) и версий JavaScript библиотечных функций C ++ (в частности, _malloc()).

  • Преобразованный код C ++ (в пределах asm) содержит прямые ссылки на внутренние библиотечные функции (также в пределах asm).

  • Ссылки на функции C ++ и многие из библиотечных функций(в частности _main, _malloc и _free) отображаются как свойства объекта asm.Они также представлены как свойства объекта Module и существуют как отдельные переменные.

Итак, исходный код C ++ будет только вызыватьвнутренняя реализация _malloc() определена в блоке кода asm.Остальная часть платформы Emscripten и любой дополнительный код JavaScript также могут вызывать эту функцию через любую из предоставленных ссылок: _malloc, Module._malloc (или Module['_malloc']) и asm._malloc (или asm['_malloc']).

Поэтому, если вы замените любую или все _malloc, Module._malloc или asm._malloc на «обернутые» версии, это повлияет только на вызовы, сделанные из остальной части платформы Emscripten или дополнительного кода JavaScript. не повлияет на вызовы, сделанные из преобразованного кода C ++.


Способы отслеживания вызовов на _malloc() / _free()

1.Официальный путь

Прежде чем мы перейдем к хакерству низкого уровня, я должен упомянуть, что Emscripten имеет встроенный Tracing API , который (согласно их странице помощи) " предоставляет некоторыеполезные возможности, чтобы лучше видеть, что происходит внутри вашего приложения, в частности, в отношении использования памяти".

Я не пытался использовать это, но для серьезной работы по отладке, это, вероятно,путьТем не менее, кажется, что нужно потратить некоторые «предварительные» усилия (вам нужно настроить отдельный процесс для получения сообщений трассировки от тестируемого приложения), поэтому в некоторых ситуациях это может быть «излишним».

Если вы хотите продолжить, официальная документация может быть найдена здесь и в этом блоге описывает, как одна компания использовала Tracing API в своих интересах (у меня нет присоединения: эта страницатолько что появился в результатах поиска).

2.Взлом

Как отмечалось выше, проблема в том, что вызовы, сделанные преобразованными вызовами C ++, относятся к внутренним функциям в объекте asm, и поэтому не подвержены влиянию каких-либо оболочек, которые мы могли бы создать «извне»уровень.После некоторых исследований я разработал два способа преодоления этой проблемы.Поскольку оба они немного «хакерские», пуристы могут захотеть отвести взгляд ...

Сначала давайте начнем с небольшого фрагмента кода, который будет служить нашей тестовой площадкой (адаптированный из того, что можно найти в * 1118).* Emscripten Tutorial стр.):

hello.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
  char* msg = malloc(1234321) ;
  strcpy( msg, "Hello, world!" ) ;
  printf( "%s\n", msg ) ;
  free( msg ) ;
  return 0;
}

Примечание : число 1234321 было выбрано просто для облегчения поиска в сгенерированном файле JavaScript. Это счастливо компилируется и работает как ожидалось:

C:\Program Files\Emscripten\Test>emcc hello.c

C:\Program Files\Emscripten\Test>node a.out.js
Hello, world!

Теперь мы создадим следующий файл JavaScript для «обтекания» malloc и free:

traceMalloc.js

Module={
  'preRun': function() {
    // Edit below or make an option to selectively wrap malloc/free.
    if( true ) {
      console.log( 'Wrapping malloc/free' ) ;
      var real_malloc = _malloc ;
      Module['_malloc'] = asm['_malloc'] = _malloc = function( size ) {
        console.log( '_malloc( ' + size + ' )' ) ;
        var result = real_malloc.apply( null, arguments ) ;
        console.log( '<--- ' + result ) ;
        return result ;
      }
      var real_free = _free ;
      Module['_free'] = asm['_free'] = _free = function( ptr ) {
        console.log( '_free( ' + ptr + ' )' ) ;
        var result = real_free.apply( null, arguments ) ;
        console.log( '<--- ' + result ) ;
        return result ;
      }
      // Hack 2b: invoke semi-permanent code added to emscripten.py
      //asm.wrapMallocFree();        }
  }
}

Module['preRun'] - это способ выполнения нашего кода незадолго до главной точки входа. Внутри функции мы сохраняем ссылку на «настоящую» подпрограмму _malloc, а затем создаем новую функцию, которая вызывает оригинал, обернутый в сообщения трассировки. Новая функция заменяет все три «внешние» ссылки на оригинальные _malloc.

(Пока игнорируйте две закомментированные строки внизу: они будут использованы позже).

Если мы скомпилируем и запустим это (используя опцию --pre-js , чтобы сказать Emscripten включить наш фрагмент JavaScript в выходной файл a.out.js), мы, как обнаружил OP, ограничены успех:

C:\Program Files\Emscripten\Test>emcc --pre-js traceMalloc.js hello.c

C:\Program Files\Emscripten\Test>node a.out.js
Wrapping malloc/free
_malloc( 42 )
<--- 5251080
_malloc( 5 )
<--- 5251128
Hello, world!

Есть два вызова на _malloc откуда-то из среды Emscripten, но тот, который нас интересует - тот, что из нашего кода C - не отслежен.

2a. One-Shot Hack

Если мы рассмотрим файл a.out.js, мы найдем следующий фрагмент, который является началом нашего кода C, преобразованного в JavaScript:

function _main() {
 var $0 = 0, $1 = 0, $2 = 0, $3 = 0, $4 = 0, $fred = 0, $vararg_buffer = 0, label = 0, sp = 0;
 sp = STACKTOP;
 STACKTOP = STACKTOP + 16|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abort();
 $vararg_buffer = sp;
 $0 = 0;
 $1 = (_malloc(1234321)|0);

Проблема в том, что вызов _malloc ссылается на внутреннюю функцию, а не на нашу переопределенную. Чтобы исправить это, мы можем отредактировать a.out.js и добавить следующие две строки вверху _main():

function _main() {
 _malloc = asm._malloc;
 _free = asm._free;

Это заменяет внутренние свойства _malloc и _free ссылками на публичные версии, хранящиеся в объекте asm (которые были заменены нашими "упакованными" версиями). Хотя это может показаться несколько округлым, оно работает (в упакованных версиях уже сохранена ссылка на функцию real malloc, поэтому они все еще вызывают ее, а не ссылка, которую мы просто перезаписано).

Если мы теперь снова запустим файл a.out.js ( без перестройка):

C:\Program Files\Emscripten\Test>node a.out.js
Wrapping malloc/free
_malloc( 42 )
<--- 5251080
_malloc( 5 )
<--- 5251128
_malloc( 1234321 )
<--- 5251144
Hello, world!
_free( 5251144 )
<--- undefined

Теперь мы можем видеть, что исходные вызовы C для malloc и free отслеживаются. Хотя это работает и легко применяется, изменения будут потеряны при следующем запуске emcc, поэтому нам придется каждый раз повторно применять исправление.

2b. Взлом рамок

Вместо того, чтобы каждый раз редактировать сгенерированный a.out.js, можно редактировать небольшую часть одного файла в платформе Emscripten, чтобы получить «исправление», которое нужно применить только один раз.

Внимание

Если вы примете этот метод, сохраните оригинальную копию файла для изменения. Кроме того, хотя я считаю предложенную мной модификацию безопасной, я не проверял ее сверх того, что требовалось для этого ответа. Используйте с осторожностью!

Файл, о котором идет речь, emscripten\1.35.0\emscripten.py находится вне основного каталога установки (по крайней мере, в Windows). Предположительно, средняя часть пути будет меняться в зависимости от версии Emscripten. Необходимо внести два изменения, вероятно, лучше всего показать это с помощью команды fc:

C:\Program Files\Emscripten\emscripten\1.35.0>fc emscripten.py.original emscripten.py
Comparing files emscripten.py.original and EMSCRIPTEN.PY
***** emscripten.py.original
    exports = []
    for export in all_exported:
***** EMSCRIPTEN.PY
    exports = []
    all_exported.append('wrapMallocFree')                 <--- Add this line
    for export in all_exported:
*****

***** emscripten.py.original
// EMSCRIPTEN_START_FUNCS
function stackAlloc(size) {
***** EMSCRIPTEN.PY
// EMSCRIPTEN_START_FUNCS
function wrapMallocFree() {                              <--- Add these lines
  console.log( 'wrapMallocFree()' ) ;                    <--- Add these lines
  _malloc = asm._malloc ;                                <--- Add these lines
  _free = asm._free ;                                    <--- Add these lines
}                                                        <--- Add these lines
function stackAlloc(size) {
*****

В моем экземпляре первое изменение находится в строке 680, а второе в строке 964. Первое изменение указывает платформе на экспорт функции wrapMallocFree из объекта asm; второе изменение определяет функцию, которая будет экспортирована. Как можно видеть, он просто выполняет те же две строки, которые мы редактировали вручную в разделе 2a (вместе с совершенно необязательной трассировкой, чтобы показать, что активация произошла).

Чтобы использовать это изменение, нам также нужно откомментировать вызов нашей новой функции в traceMalloc.js, чтобы она гласила:

        return result ;
      }
      // Hack 2b: invoke semi-permanent code added to emscripten.py
      asm.wrapMallocFree();        }
  }
}

Теперь мы можем пересобрать и повторно запустить код и , чтобы увидеть все отслеженные вызовы без ручного редактирования a.out.js:

C:\Program Files\Emscripten\Test>emcc --pre-js traceMalloc.js hello.c

C:\Program Files\Emscripten\Test>node a.out.js
Wrapping malloc/free
wrapMallocFree()
_malloc( 42 )
<--- 5251080
_malloc( 5 )
<--- 5251128
_malloc( 1234321 )
<--- 5251144
Hello, world!
_free( 5251144 )
<--- undefined

Как предполагает if( true ) ... бит traceMalloc.js, мы можем оставить изменения на emscripten.py на месте и выборочно включить или выключить трассировку malloc и free. Когда не используется, единственный эффект состоит в том, что asm экспортирует еще одну функцию (wrapMallocFree), которая никогда не будет вызвана. Из того, что я вижу в остальной части этого файла, это не должно вызывать каких-либо проблем (больше никто не узнает, что это там). Даже если ваш код на C / C ++ должен был содержать функцию с именем wrapMallocFree, поскольку такие имена начинаются с символа подчеркивания (main становится _main и т. Д.), Конфликт не должен быть.

Очевидно, что если вы переключитесь на другую версию Emscripten, вам потребуется повторно применить те же (или похожие) изменения.


Все подробности Гори

Как и было обещано, некоторые детали того, что происходит с malloc внутри сгенерированного кода Emscripten.

Вещи "ненадежные"

Как отмечалось выше, очень большой кусок сгенерированного a.out.js (около 60% для тестовой программы) состоит из создания asm объекта. Этот код заключен в скобки EMSCRIPTEN_START_ASM и EMSCRIPTEN_END_ASM и на довольно высоком уровне выглядит следующим образом:

// EMSCRIPTEN_START_ASM
var asm = (function(global, env, buffer) {

   ...

   function _main() {
      ...
      $1 = (_malloc(1234321)|0);
      ...
   }

   ...

   function _malloc($bytes) {
      ...
      return ($mem$0|0);
   }

   ...

   return { ... _malloc: _malloc, ... };
})
// EMSCRIPTEN_END_ASM
(Module.asmGlobalArg, Module.asmLibraryArg, buffer);

Объект asm определен с использованием шаблона немедленно вызванного выражения функции (IIFE) . По сути, весь блок определяет анонимную функцию, которая выполняется немедленно. Результатом выполнения этой функции является то, что назначено объекту asm. Это выполнение происходит в тот момент, когда встречается вышеуказанный код. Суть «IIFE» заключается в том, что переменные / функции, определенные в , что анонимная функция, видны только коду внутри этой функции. Все, что видит «внешний мир» - это то, что возвращает эта функция (которая назначена на asm).

Для нас интересны определения как _main (преобразованный код C), так и _malloc (реализация Emscripten для выделения памяти). Из-за того, как работают JavaScript / IIFE, при выполнении кода в _main его вызов _malloc всегда будет ссылаться на эту внутреннюю версию _malloc.

Возвращаемое значение IIFE - это объект с рядом свойств. Случается, что имена свойств этого объекта совпадают с именами объектов / функций в анонимной функции. Хотя это может показаться запутанным, не возникает никакой двусмысленности. Возвращенный объект (назначенный asm) имеет свойство с именем _malloc. Значение этого свойства устанавливается равным значению внутреннего объекта _malloc (определение функции по существу создает свойство / объект, который ссылается на «блок кода») это тело функции. Этой ссылкой можно манипулировать, как и всеми другими ссылками).

Определение Module

Вскоре после строительства у нас есть следующий блок кода:

var _free = Module["_free"] = asm["_free"];
var _main = Module["_main"] = asm["_main"];
var _i64Add = Module["_i64Add"] = asm["_i64Add"];
var _memset = Module["_memset"] = asm["_memset"];
var runPostSets = Module["runPostSets"] = asm["runPostSets"];
var _malloc = Module["_malloc"] = asm["_malloc"];

Для выбранных свойств вновь созданного объекта asm это делает две вещи: (a) создает свойства во втором объекте (Module), который ссылается на то же самое, что и свойство asm, и (b) создает некоторые глобальные переменные, которые также ссылаются на эти свойства. Глобальные переменные предназначены для использования другими частями платформы Emscripten; объект Module предназначен для использования другим кодом JavaScript, который может быть добавлен в код, сгенерированный Emscripten.

Все дороги ведут к _malloc

На данный момент мы имеем следующее:

  • Существует блок кода, определенный в анонимной функции, используемой для создания asm, который обеспечивает реализацию / эмуляцию Emscripten функции C * C ++ _malloc. Этот код является "настоящим malloc". Следует отметить, что этот код «существует» более или менее независимо от того, какие объекты / свойства (если таковые имеются) «ссылаются» на него.

  • Существует внутренний объект IIFE с именем _malloc, который в настоящее время ссылается на приведенный выше код.Вызовы malloc(), сделанные исходным кодом C / C ++, будут выполняться с использованием значения этого объекта.

  • У объекта asm есть свойство с именем _malloc, что также в настоящее время ссылается на вышеуказанный блок кода.

  • Объект Module также имеет свойство с именем _malloc, которое в настоящее время ссылается на вышеуказанный блоккода.

  • Существует глобальный объект _malloc.Неудивительно, что также ссылается на вышеуказанный блок кода.

На данный момент, используя _malloc (global-scope), Module._malloc (или * 1364)*, asm._malloc или _malloc (в рамках IIFE, использованного для построения asm) all окажутся в одном и том же блоке кода - «реальная» реализация malloc().

Когда выполняется следующий фрагмент кода (в контексте function):

      var real_malloc = _malloc ;
      Module['_malloc'] = asm['_malloc'] = _malloc = function( size ) {
        console.log( '_malloc( ' + size + ' )' ) ;
        var result = real_malloc.apply( null, arguments ) ;
        console.log( '<--- ' + result ) ;
        return result ;
      }

, тогда происходит несколько вещей:

  • Сделана копия исходного значения (глобального) объекта _malloc (real_malloc). Это, как мы видели выше, содержит ссылку на «реальный» блок кода, который реализует malloc().это оказывается таким же значением , что и внутренний объект IIFE _malloc, между ними нет никакой связи. Если / когда значение внутреннего элемента IIFE _malloc изменяется, оно будет not влияет на значение, хранящееся в real_malloc.

  • Создается новая (анонимная) функция, содержащая вызов «реальной» реализацииmalloc() (используя созданный выше объект real_malloc), а также некоторые сообщения журнала для отслеживания вызова.

  • Ссылки на эту новую функцию хранятся в трех "внешние "объекты, которые мы упоминали выше: _malloc (global-scope), Module._malloc и asm._malloc.Внутренний объект IIFE _malloc по-прежнему указывает на «реальную реализацию» malloc().

Сейчас мы находимся на этапе, на котором ОП получил: external вызовы malloc() (сделанные из платформы Emscripten или других фрагментов кода JavaScript) будут направлены через функции-оболочки и могут быть отслежены.Вызовы, сделанные из преобразованного кода C / C ++ - который использует внутренний объект IIFE _malloc - все еще направлены на "реальную" реализацию и не отслеживаются.

Когда выполняется следующее в контексте анонимной функции IIFE :

_malloc = asm._malloc ;

Тогда (и только тогда) внутренний объект IIFE _malloc будет изменен.К тому времени, когда это выполнено, его новое значение (asm._malloc) ссылается на нашу функцию-обертку.В этот момент все четыре варианта «links-to-malloc» указывают на нашу функцию «обертки».Эта функция все еще имеет доступ (через переменную real_malloc) к «реальной» реализации malloc(), поэтому теперь, когда любая часть кода вызывает malloc(), этовызов проходит через нашу функцию-обертку, поэтому его можно отследить.

...