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

deg*_*gen 2 c++ emscripten

我通过包装原始函数并仅添加 Console.log 来显示内存地址、大小和分配的总内存,从而覆盖 Javascript(emscripten) 中的 Module._malloc 和 Module._free。

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

根据Ofria先生的回答/sf/answers/2384014391/,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);
}
Run Code Online (Sandbox Code Playgroud)

Tri*_*und 6

简短的回答:您尝试包装malloc/free不起作用,因为公开Emscripten 的/实现的Module对象不是本机 C++ 代码调用入口点。然而,通过一些技巧,您可以通过一些方法来追踪这些调用。malloc()free()

\n\n
\n\n

为什么你的覆盖不起作用

\n\n

我认为您引用的答案可能更好地表述为:C++ 的模拟malloc()和调用在andfree()中公开,但这些不是转换后的 C++ 代码调用的入口点。Module._malloc()Module._free()

\n\n

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

\n\n

我将把 Emscripten 如何处理的所有细节留malloc()到后面,但简单地说:

\n\n
    \n
  • 使用“标准设置”,Emscripten 将 C++ 程序编译为a.out.js.

  • \n
  • 该文件的很大一部分创建了一个asm对象。它包含所有转换后的 C++ 代码(例如 的 JavaScript 实现_main()C++ 库函数的 JavaScript 版本(特别是_malloc())。

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

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

  • \n
\n\n

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

\n\n

_malloc因此,如果您将 或 的任何或全部替换为“包装”版本,则这只会影响从 Emscripten 框架的其余部分或其他 JavaScript 代码进行的调用Module._mallocasm._malloc它不会影响从转换后的 C++ 代码进行的调用。

\n\n
\n\n

追踪呼叫_malloc()/的方法_free()

\n\n

1. 官方方式

\n\n

在我们讨论一些低级黑客之前,我应该提到 Emscripten 有一个内置的跟踪 API (根据他们的帮助页面)“提供了一些有用的功能,可以更好地了解应用程序内部发生的情况,特别是在方面内存使用情况”。

\n\n

我没有尝试使用它,但对于认真的调试工作,这可能是正确的方法。然而,它似乎需要一些“预先”的努力(您需要设置一个单独的进程来接收来自被测应用程序的跟踪消息),因此在某些情况下它可能是“矫枉过正”。

\n\n

如果您想了解这一点,可以在此处找到官方文档,这篇博文描述了一家公司如何使用 Tracing API 来发挥其优势(我没有隶属关系:该页面刚刚出现在搜索结果中)。

\n\n

2. 破解它

\n\n

如上所述,问题在于转换后的 C++ 调用所进行的调用是对象内的内部函数asm,因此不会受到我们可能在“外部”级别创建的任何包装器的影响。经过一番调查,我设计了两种方法来解决这个问题。由于两者都有点“hacky”,纯粹主义者可能想把目光移开……

\n\n

首先,让我们从一小段代码开始作为我们的测试平台(改编自Emscripten 教程页面上的代码):

\n\n

hello.c

\n\n
#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nint main() {\n  char* msg = malloc(1234321) ;\n  strcpy( msg, "Hello, world!" ) ;\n  printf( "%s\\n", msg ) ;\n  free( msg ) ;\n  return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

注意1234321:选择该数字只是为了帮助搜索生成的 JavaScript 文件。这很高兴地按预期编译并运行:

\n\n
C:\\Program Files\\Emscripten\\Test>emcc hello.c\n\nC:\\Program Files\\Emscripten\\Test>node a.out.js\nHello, world!\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们现在将创建以下 JavaScript 文件来“包装”mallocfree

\n\n

traceMalloc.js

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

Module[\'preRun\']是一种让我们的代码在主入口点之前不久执行的方法。在函数内部,我们保存对“真实”_malloc例程的引用,然后创建一个新函数来调用原始函数,并封装在跟踪消息中。新函数替换了对原始_malloc.

\n\n

(现在,忽略底部附近的两行注释掉的行:稍后将使用它们)。

\n\n

如果我们编译并运行它(使用--pre-js选项告诉 Emscripten 将我们的 JavaScript 片段包含在输出a.out.js文件中),正如 OP 所发现的那样,我们只取得了有限的成功:

\n\n
C:\\Program Files\\Emscripten\\Test>emcc --pre-js traceMalloc.js hello.c\n\nC:\\Program Files\\Emscripten\\Test>node a.out.js\nWrapping malloc/free\n_malloc( 42 )\n<--- 5251080\n_malloc( 5 )\n<--- 5251128\nHello, world!\n
Run Code Online (Sandbox Code Playgroud)\n\n

Emscripten 框架中的某个位置有两个调用_malloc,但我们感兴趣的一个调用是 \xe2\x80\x93,而来自我们的 C 代码 \xe2\x80\x93 的调用尚未被追踪到。

\n\n

2a. 一击破解

\n\n

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

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

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

\n\n
function _main() {\n _malloc = asm._malloc;\n _free = asm._free;\n
Run Code Online (Sandbox Code Playgroud)\n\n

这替换了内部属性_malloc以及对对象持有的公共版本的_free引用(到目前为止,它们已被我们的“包装”版本替换)。虽然这看起来有点循环,但它是有效的(包装版本已经存储了对真实函数的引用,因此它们仍然调用它,而不是我们刚刚覆盖的引用)。asm malloc

\n\n

如果我们现在重新运行该a.out.js文件(重建):

\n\n
C:\\Program Files\\Emscripten\\Test>node a.out.js\nWrapping malloc/free\n_malloc( 42 )\n<--- 5251080\n_malloc( 5 )\n<--- 5251128\n_malloc( 1234321 )\n<--- 5251144\nHello, world!\n_free( 5251144 )\n<--- undefined\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们现在可以看到原始的 C 调用mallocfree正在被跟踪。虽然这有效并且很容易应用,但下次运行时更改将丢失,emcc因此我们每次都必须重新应用修复。

\n\n

2b. 破解框架

\n\n

无需a.out.js每次都编辑生成的文件,而是可以在 Emscripten 框架中编辑一个文件的一部分,以获得只需应用一次的“修复”。

\n\n
\n

警告

\n\n

如果采用此方法,请保留要修改的文件的原始副本。另外,虽然我相信我建议的修改是安全的,但我还没有对其进行超出此答案所需的测试。请谨慎使用!

\n
\n\n

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

\n\n
C:\\Program Files\\Emscripten\\emscripten\\1.35.0>fc emscripten.py.original emscripten.py\nComparing files emscripten.py.original and EMSCRIPTEN.PY\n***** emscripten.py.original\n    exports = []\n    for export in all_exported:\n***** EMSCRIPTEN.PY\n    exports = []\n    all_exported.append(\'wrapMallocFree\')                 <--- Add this line\n    for export in all_exported:\n*****\n\n***** emscripten.py.original\n// EMSCRIPTEN_START_FUNCS\nfunction stackAlloc(size) {\n***** EMSCRIPTEN.PY\n// EMSCRIPTEN_START_FUNCS\nfunction wrapMallocFree() {                              <--- Add these lines\n  console.log( \'wrapMallocFree()\' ) ;                    <--- Add these lines\n  _malloc = asm._malloc ;                                <--- Add these lines\n  _free = asm._free ;                                    <--- Add these lines\n}                                                        <--- Add these lines\nfunction stackAlloc(size) {\n*****\n
Run Code Online (Sandbox Code Playgroud)\n\n

在我的副本中,第一个更改位于第 680 行,第二个更改位于第 964 行。第一个更改告诉框架从对象导出函数;第二个更改告诉框架wrapMallocFree从对象中导出函数asm。第二个更改定义了将导出的函数。可以看出,这只是执行与我们在第2a节中手动编辑的相同的两行(以及完全可选的跟踪行,以显示激活已经发生)。

\n\n

要利用此更改,我们还需要取消对新函数的调用的注释,如下traceMalloc.js所示:

\n\n
        return result ;\n      }\n      // Hack 2b: invoke semi-permanent code added to emscripten.py\n      asm.wrapMallocFree();        }\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,我们可以重新构建并重新运行代码查看跟踪的所有调用,而无需手动编辑a.out.js

\n\n
C:\\Program Files\\Emscripten\\Test>emcc --pre-js traceMalloc.js hello.c\n\nC:\\Program Files\\Emscripten\\Test>node a.out.js\nWrapping malloc/free\nwrapMallocFree()\n_malloc( 42 )\n<--- 5251080\n_malloc( 5 )\n<--- 5251128\n_malloc( 1234321 )\n<--- 5251144\nHello, world!\n_free( 5251144 )\n<--- undefined\n
Run Code Online (Sandbox Code Playgroud)\n\n

正如 的if( true ) ...建议traceMalloc.js,我们可以将更改保留emscripten.py在适当的位置,并有选择地打开或关闭malloc和的跟踪free。不使用时,唯一的效果是asm导出一个wrapMallocFree永远不会被调用的函数 ( )。从我对该文件其余部分的了解来看,这不会导致任何问题(其他任何东西都不会知道它在那里)。即使您的 C/C++ 代码包含一个名为 的函数wrapMallocFree,因为此类名称带有下划线前缀(main变为_main等),也不应该发生冲突。

\n\n

显然,如果您切换到不同版本的 Emscripten,则需要重新应用相同(或类似)的更改。

\n\n
\n\n

所有血淋淋的细节

\n\n

malloc正如所承诺的, Emscripten 生成的代码内部发生的一些细节。

\n\n

事情变得“不确定”

\n\n

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

\n\n
// EMSCRIPTEN_START_ASM\nvar asm = (function(global, env, buffer) {\n\n   ...\n\n   function _main() {\n      ...\n      $1 = (_malloc(1234321)|0);\n      ...\n   }\n\n   ...\n\n   function _malloc($bytes) {\n      ...\n      return ($mem$0|0);\n   }\n\n   ...\n\n   return { ... _malloc: _malloc, ... };\n})\n// EMSCRIPTEN_END_ASM\n(Module.asmGlobalArg, Module.asmLibraryArg, buffer);\n
Run Code Online (Sandbox Code Playgroud)\n\n

该对象asm是使用立即调用函数表达式 (IIFE) 模式定义的。本质上,整个块定义了一个立即执行的匿名函数。执行该函数的结果就是分配给对象的结果asm。该执行发生在遇到上述代码时。“IIFE”的要点是匿名函数定义的变量/函数仅对该函数中的代码可见。所有“外部世界”看到的都是该函数返回的内容(分配给asm)。

\n\n

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

\n\n

IIFE 的返回值是一个具有许多属性的对象。碰巧该对象的属性名称恰好与匿名函数中的对象/函数名称相同。虽然这可能看起来令人困惑,但并不存在歧义。返回的对象(分配给asm)有一个名为 的属性_malloc该属性的值设置为等于内部对象的_malloc(函数的定义本质上创建一个属性/对象,该属性/对象引用作为函数主体的“代码块”。可以像所有操作一样操作该引用其他参考)。

\n\n

的定义Module

\n\n

构建后不久,我们有以下代码块:

\n\n
var _free = Module["_free"] = asm["_free"];\nvar _main = Module["_main"] = asm["_main"];\nvar _i64Add = Module["_i64Add"] = asm["_i64Add"];\nvar _memset = Module["_memset"] = asm["_memset"];\nvar runPostSets = Module["runPostSets"] = asm["runPostSets"];\nvar _malloc = Module["_malloc"] = asm["_malloc"];\n
Run Code Online (Sandbox Code Playgroud)\n\n

对于新创建的对象的选定asm属性,这会执行两件事:(a)在第二个对象 ( ) 中创建属性Module,该属性引用与 does 的属性相同的内容asm,以及(b)创建一些全局变量,这些变量也引用这些属性。全局变量供 Emscripten 框架的其他部分使用;该Module对象供可能添加到 Emscripten 生成的代码中的其他 JavaScript 代码使用。

\n\n

条条大路通向_malloc

\n\n

此时,我们有以下内容:

\n\n
    \n
  • 在用于创建的匿名函数中定义了一个代码块asm,它提供了 Emscripten 对 C/C++ 函数的实现/模拟_malloc。这段代码是“真正的malloc”。应该注意的是,该代码“存在”或多或少独立于任何对象/属性(如果有)“引用”它。

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

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

  • \n
  • 该对象Module 还有一个名为 的属性_malloc,该属性当前引用上述代码块。

  • \n
  • 有一个全局对象_malloc。毫不奇怪,它引用了上面的代码块。

  • \n
\n\n

此时,使用_malloc(global-scope)、Module._malloc(or Module[\'_malloc\']asm._mallocor _malloc(在用于构建 的 IIFE 内asm)将最终得到同一代码块 \xe2\x80\x93 的“真正”实现malloc()

\n\n

当执行以下代码片段时(在上下文中function):

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

然后发生了几件事:

\n\n
    \n
  • _malloc生成(全局)对象的原始值的副本( real_malloc)。正如我们在上面看到的,它保存了对实现malloc(). 虽然这恰好与 IIFE 内部对象的值相同_malloc,但两者之间没有任何联系。如果/当 IIFE-internal 的值_malloc发生更改时,它不会影响中保存的值real_malloc

  • \n
  • 创建一个新的(匿名)函数。它包含对“真实”实现的调用malloc()(使用上面创建的对象real_malloc)以及一些跟踪调用的日志消息。

  • \n
  • 对这个新函数的引用存储在我们上面提到的三个“外部”对象中:_malloc(global-scope)Module._mallocasm._malloc. IIFE 内部对象_malloc仍然指向 的“实际实现” malloc()

  • \n
\n\n

我们现在正处于 OP 所处的阶段:外部调用malloc()(由 Emscripten 框架或其他 JavaScript 代码进行)将通过“包装器”函数进行传递,并且可以被追踪。从使用 IIFE 内部对象 \xe2\x80\x93 的转换后的 C/C++ 代码 \ _mallocxe2\x80\x93 进行的调用仍然定向到“真实”实现并且不会被跟踪。

\n\n

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

\n\n
_malloc = asm._malloc ;\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n