如何在 javascript (emscripten) 中覆盖 c++ malloc/free?

How to override c++ malloc/free in javascript (emscripten)?

我覆盖了 Javascript(emscripten) 中的 Module._malloc 和 Module._free,方法是包装原始函数并添加 Console.log 以显示内存地址、大小和总分配内存。

我发现新函数仅捕获 Javascript 对 Module._malloc 和 Module._free 的调用,而不会捕获对 malloc() 和 free() 的 c++ 级调用。我想知道为什么。

根据Ofria先生在这里的回答,Module._malloc和Module._free是c++的malloc()和free()转换后的等价代码。

我正在使用 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);
}

简短回答:您尝试包装 malloc/free 无效,因为 Module 对象 exposes Emscripten 的 malloc()/free() 实现 而不是 本机 C++ 代码调用的 entry-points。但是,只要稍加技巧,您就可以 跟踪这些调用。


为什么您的覆盖不起作用

我认为您引用的答案措辞可能更好:C++ 的 malloc()free() 调用的 仿真 在 [=31= 中公开] 和 Module._free(),但这些 不是 转换后的 C++ 代码调用的 entry-points。

注意:我通常只会在这个答案的剩余部分谈论malloc,但基本上适用于malloc的所有内容也适用于free.

我将把 Emscripten 如何处理 malloc() 的所有血腥细节留到以后,但简而言之:

  • 使用"standard settings",Emscripten将C++程序编译成a.out.js.

  • 此文件的一大块内容创建了一个 asm 对象。这包含所有转换后的 C++ 代码(例如 _main() 的 JavaScript 实现) JavaScript 版本的 C++ 库函数(特别是 _malloc()).

  • 转换后的 C++ 代码(在 asm 内)直接引用内部库函数(也在 asm 内)。

  • 对 C++ 函数和许多库函数(特别是 _main_malloc_free)的引用作为 asm 对象。它们 作为 Module 对象的属性公开,并作为独立变量存在。

因此,原始 C++ 代码将 调用 asm 代码块中定义的 _malloc() 的内部实现。 Emscripten 框架的其余部分,以及任何额外的 JavaScript 代码也可以通过任何公开的引用调用此函数:_mallocModule._malloc(或 Module['_malloc'])和 asm._malloc(或asm['_malloc'])。

因此,如果您将 _mallocModule._mallocasm._malloc 中的任何一个或全部替换为 "wrapped" 版本,这只会影响来自其余版本的调用Emscripten 框架或额外的 JavaScript 代码。它将不会影响从转换后的 C++ 代码进行的调用。


跟踪调用到 _malloc()/_free()

的方法

1。官方方式

在我们进入一些 low-level hackery 之前,我应该提到 Emscripten 有一个 Tracing API built-in 其中(根据他们的帮助页)“提供了一些有用的功能来更好地查看应用程序内部发生的事情,特别是在内存使用方面”。

我没有尝试使用它,但对于认真的调试工作,这可能是可行的方法。但是,它似乎需要一些 "up-front" 的努力(您需要设置一个单独的进程来接收来自被测应用程序的跟踪消息),因此在某些情况下可能 "overkill"。

如果你想继续这个,官方文档 can be found here and this blog post 描述了一家公司如何使用 Tracing API 来发挥他们的优势(我没有隶属关系:那个页面刚刚出现在搜索结果中) .

2。破解它

如上所述,问题在于转换后的 C++ 调用是针对 asm 对象中的内部函数,因此不受我们可能在 [=437= 中创建的任何包装器的影响] 等级。经过一些调查,我设计了两种方法来克服这个问题。由于两者都有点"hacky",纯粹主义者可能想把目光移开...

首先,让我们从一小段代码开始作为我们的 test-bed(改编自 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文件。这愉快地编译并按预期 运行s:

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

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

我们现在将创建以下 JavaScript 文件到 "wrap" mallocfree:

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'] 是一种让我们的代码在主 entry-point 之前执行的方法。在函数内部,我们保存对 "real" _malloc 例程的引用,然后创建一个调用原始函数的新函数,包装在 trace-messages 中。新函数替换了对原始 _malloc.

的所有三个 "external" 引用

(暂时忽略底部附近的两行commented-out:稍后会用到它们)。

如果我们编译 运行 这个(使用 --pre-js option 告诉 Emscripten 在输出 a.out.js 文件中包含我们的 JavaScript 片段),我们有,正如 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!

在 Emscripten 框架的某个地方有两次对 _malloc 的调用,但是我们感兴趣的那个——来自我们的 C 代码的那个——还没有被追踪到。

2a。 One-Shot黑客

如果我们检查在a.out.js文件中,我们会发现下面的片段,这是我们的C代码开始转换为JavaScript:

function _main() {
 var [=14=] = 0,  = 0,  = 0,  = 0,  = 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;
 [=14=] = 0;
  = (_malloc(1234321)|0);

问题是对 _malloc 的调用引用了 内部 函数,而不是我们重写的函数。要解决这个问题,我们可以 edit a.out.js_main() 的顶部添加以下两行:

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

这会将内部属性 _malloc_free 替换为对 asm 对象持有的 public 版本的引用(到目前为止,已经被我们的 "wrapped" 版本取代了)。虽然这看起来有点循环,但它确实有效(包装版本已经存储了对 real malloc 函数的引用,所以他们仍然调用它,而 我们刚刚覆盖的引用)。

如果我们现在re-runa.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

我们现在可以看到对 mallocfree 的原始 C 调用正在被跟踪。虽然这行得通并且易于应用,但下次我们 运行 emcc 时更改将丢失,因此我们每次都必须 re-apply 修复。

2b。破解框架

不是每次都编辑生成的 a.out.js,而是可以在 Emscripten 框架中编辑一个文件的 small 部分以获得 "fix" 只需申请一次。

Warning

If you adopt this method keep an original copy of the file to be modified. Also, while I believe my suggested modification to be safe, I have not tested it beyond what was needed for this answer. Use with due caution!

有问题的文件位于主安装目录 emscripten.35.0\emscripten.py 之外(至少在 Windows 下)。想必路径的中间部分会随着不同版本的Emscripten而改变。需要进行两项更改,最好使用 fc 命令的输出显示:

C:\Program Files\Emscripten\emscripten.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 行。第一个更改告诉框架从 asm 对象导出函数 wrapMallocFree;第二个更改定义了将要导出的函数。可以看出,这只是执行了与我们在 2a 部分手动编辑的相同的两行(以及完全可选的 trace-line,以显示激活已经发生)。 =166=]

要利用此更改,我们还需要 un-comment 调用 traceMalloc.js 中的新函数,因此它显示为:

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

现在,我们可以re-build和re-run代码看到所有跟踪的呼叫而无需手动编辑的 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

正如 traceMalloc.jsif( true ) ... 位所建议的那样,我们可以保留对 emscripten.py 的更改,并有选择地打开或关闭 malloc 和 [= 的跟踪25=]。不使用时,唯一的影响是 asm 导出一个永远不会被调用的函数 (wrapMallocFree)。从我对该文件其余部分的了解来看,这应该不会造成任何问题(没有其他人会知道它在那里)。即使您的 C/C++ 代码包含一个名为 wrapMallocFree 的函数,因为这样的名称带有下划线前缀(main 变为 _main 等),也应该有没有冲突。

显然,如果您切换到不同版本的 Emscripten,则需要 re-apply 相同(或相似)的更改。


所有血淋淋的细节

正如承诺的那样,Emscripten 生成的代码中 malloc 发生的一些细节。

事情得到'iffy'

如上所述,生成的 a.out.js 中有很大一部分(测试程序约占 60%)包含 asm 对象的创建。此代码由 EMSCRIPTEN_START_ASMEMSCRIPTEN_END_ASM 括起来,在相当高的级别上看起来像:

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

   ...

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

   ...

   function _malloc($bytes) {
      ...
      return ($mem[=20=]|0);
   }

   ...

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

对象 asm 是使用 immediately invoked function expression (IIFE) pattern 定义的。本质上,整个块定义了一个立即执行的匿名函数。执行该函数的结果是分配给对象 asm 的结果。此执行发生在遇到上述代码时。 "IIFE" 的要点是 variables/functions 在 中定义了 匿名函数仅对该函数中的代码可见。 "outside world" 看到的是函数 returns(分配给 asm)的任何内容。

我们感兴趣的是,我们看到了 _main(转换后的 C 代码)和 _malloc(Emscripten 的内存分配器实现)的定义。由于 JavaScript/IIFEs 的工作方式,当执行 _main 中的代码时,对 _malloc 的调用将始终引用 _malloc.

的内部版本

IIFE 的return 值是一个具有多个属性的对象。碰巧这个对象的属性名称恰好与匿名函数中 objects/functions 的名称相同。虽然这可能看起来令人困惑,但不涉及歧义。 returned 对象(分配给 asm)有一个名为 _malloc 的 属性。 属性 的 value 设置为等于内部对象 _mallocvalue (函数的定义本质上是创建一个 property/object 来引用作为函数主体的 "block of code"。这个引用可以是 manipula像所有其他参考资料一样编辑)。

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"];

对于 newly-created asm 对象的 selected 属性,这会做两件事:(a)它在第二个对象 (Module) 中创建属性,该对象引用与 asm 的 属性 相同的内容,并且 (b) 它创建一些也引用这些属性的全局变量。全局变量供 Emscripten 框架的其他部分使用; Module 对象供可能添加到 Emscripten-generated 代码的其他 JavaScript 代码使用。

条条大路通_malloc

此时,我们有以下内容:

  • 在用于创建 asm 的匿名函数中定义了一段代码,它提供了 Emscripten 的 implementation/emulation 的 C/C++ 的 _malloc函数。这个代码就是"real malloc"。需要注意的是,这段代码 "exists" more-or-less 独立于任何 objects/properties (如果有的话) "reference" 它。

  • IIFE 有一个名为 _malloc 的内部对象,当前 引用了上述代码。原始 C/C++ 代码对 malloc() 的调用将使用此对象的值进行。

  • 对象 asm 有一个名为 _malloc 的 属性, 当前引用上述代码块。

  • 对象 Module 有一个 属性 称为 _malloc 当前引用上述代码块。

  • 有一个全局对象_malloc。不出所料,它引用了上面的代码块。

此时,使用_malloc(global-scope)、Module._malloc(或Module['_malloc']asm._malloc_malloc(在用于构建 asm) 的 IIFE 将 all 以相同的代码块结束 – "real" 实现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() 的 "real" 代码块的引用。虽然此 恰好与 IIFE-internal 对象 _malloc 具有相同的值,但两者之间没有任何联系。 If/whenIIFE-internal_malloc的值改变了,不会影响real_malloc的值。

  • 创建了一个新的(匿名)函数。它包含对 malloc() 的 "real" 实现的调用(使用上面创建的对象 real_malloc)以及一些 log-messages 来跟踪调用。

  • 对这个新函数的引用存储在我们上面提到的三个 "outside" 对象中:_malloc (global-scope)、Module._mallocasm._malloc。 IIFE-internal 对象 _malloc 仍然指向 malloc() 的 "real implementation"。

我们现在处于 OP 到达的阶段:外部 调用 malloc()(由 Emscripten 框架或其他 JavaScript 代码生成) 将通过 "wrapper" 函数汇集并可被追踪。从转换后的 C/C++ 代码(使用 IIFE-internal 对象 _malloc)进行的调用仍然指向 "real" 实现,不会被跟踪。

当在匿名 IIFE 函数的上下文中执行以下 :

_malloc = asm._malloc ;

然后(并且只有到那时)IIFE-internal 对象 _malloc 才会被更改。执行此操作时,它的新值 (asm._malloc) 正在引用我们的 "wrapper" 函数。那时 "references-to-malloc" 的所有四个 变体都指向我们的 "wrapper" 函数。该函数仍然可以(通过变量 real_malloc)访问 malloc() 的 "real" 实现,所以现在,每当 any 部分代码调用 malloc(),该调用通过我们的包装函数,因此可以跟踪调用。