首页 > 解决方案 > 如何在 javascript (emscripten) 中覆盖 c++ malloc/free?

问题描述

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

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

根据 Ofria 先生在此处https://stackoverflow.com/a/34057348/4806940的回答,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);
}

标签: c++emscripten

解决方案


简短的回答:您尝试包装malloc/free不起作用,因为暴露Emscripten 的/实现的Module对象不是本机 C++ 代码调用的入口点。但是,通过一些技巧,您可以通过多种方式跟踪这些调用。malloc()free()


为什么你的覆盖不起作用

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

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

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

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

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

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

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

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

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


跟踪呼叫的方式_malloc()/_free()

1.官方方式

在我们进入一些低级黑客之前,我应该提到 Emscripten 有一个内置的跟踪 API (根据他们的帮助页面)“提供一些有用的功能来更好地查看应用程序内部正在发生的事情,特别是关于到内存使用”。

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

如果你想追求这个,官方文档可以在这里找到这篇博文描述了一家公司如何利用 Tracing API 来发挥自己的优势(我没有从属关系:该页面刚刚出现在搜索结果中)。

2. 破解它

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

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

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 文件来“包装”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']是一种让我们的代码在主入口点之前不久执行的方法。在函数内部,我们保存对“真实”_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!

在 Emscripten 框架的某个地方有两个调用_malloc,但我们感兴趣的一个 - 来自我们的 C 代码的一个 - 没有被跟踪。

2a. 一键破解

如果我们检查该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 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 调用mallocfree正在被跟踪。虽然这很有效,并且很容易应用,但下次运行时更改将丢失,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 行。第一个更改告诉框架wrapMallocFreeasm对象中导出函数;第二个更改定义了将要导出的函数。可以看出,这只是执行了与我们在第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在原处,并有选择地打开或关闭对mallocand的跟踪free。不使用时,唯一的作用是asm导出一个wrapMallocFree永远不会被调用的函数 ( )。从我可以看到该文件的其余部分,这不应该引起任何问题(没有其他人会知道它在那里)。即使您的 C/C++ 代码包含一个名为 的函数wrapMallocFree,因为这些名称的前缀是下划线(main变得_main等),也不应该发生冲突。

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


所有血腥细节

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

事情变得“不确定”

如上所述,生成的很大一部分a.out.js(大约 60% 用于测试程序)由asm对象的创建组成。这段代码由EMSCRIPTEN_START_ASMand括起来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在第二个对象(那些属性。全局变量供 Emscripten 框架的其他部分使用;该对象供可能添加到 Emscripten 生成的代码中的其他 JavaScript 代码使用。asmModule

条条大路通_malloc

此时,我们有以下内容:

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

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

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

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

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

此时,使用_malloc(global-scope), Module._malloc(or Module['_malloc'], asm._mallocor _malloc(在用于构建的 IIFE 内asm) 都将同一个代码块中结束 - 的“真正”实现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-internal object的值相同_malloc,但两者之间没有联系。如果/当 IIFE-internal 的_malloc值发生变化时,它不会影响real_malloc.

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

  • 对这个新函数的引用存储在我们上面提到的三个“外部”对象中:(_malloc全局范围)Module._mallocasm._malloc. IIFE 内部对象_malloc仍然指向malloc().

我们现在处于 OP 的阶段:外部调用malloc()(来自 Emscripten 框架或其他 JavaScript 代码)将通过“包装器”函数汇集并可以跟踪。从转换后的 C/C++ 代码(使用 IIFE 内部对象_malloc)进行的调用仍被定向到“真实”实现并且不被跟踪。

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

_malloc = asm._malloc ;

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


推荐阅读