WebAssembly C程序malloc随机崩溃问题排查求助
问题:WebAssembly打包工具处理大型项目时malloc崩溃
我正在开发一个用C和少量JavaScript编写的JavaScript/Web模块打包工具。该工具处理小型项目完全正常,但处理文件较多的复杂项目时,会随机在某个函数的malloc调用处崩溃。此前遇到过由程序其他部分内存泄漏/错误导致的malloc和free报错,但目前未发现明显异常。
报错信息
RuntimeError: memory access out of bounds at dlmalloc (wasm://wasm/007a3c2a:wasm-function[339]:0x7db59) at ReadDataFromFile (wasm://wasm/007a3c2a:wasm-function[114]:0x4cfd8) at BundleFile (wasm://wasm/007a3c2a:wasm-function[128]:0x55e36) at BundleFiles (wasm://wasm/007a3c2a:wasm-function[130]:0x6b9fb) at /mnt/c/Users/redstarbird/Documents/Projects/ArrowStack/Build/CFunctions.js:985:22 at Object.ccall (/mnt/c/Users/redstarbird/Documents/Projects/ArrowStack/Build/CFunctions.js:5328:22) at /mnt/c/Users/redstarbird/Documents/Projects/ArrowStack/src/ArrowPack.js:104:24
崩溃函数代码
char EMSCRIPTEN_KEEPALIVE *ReadDataFromFile(char *path) { // 返回文件内容 printf("path: %s\n", path); FILE *filePTR = fopen(path, "r"); if (filePTR == NULL) { printf("Error opening file %s\n", path); return NULL; } fseek(filePTR, 0, SEEK_END); // 定位到文件末尾 long int length = ftell(filePTR); // 获取文件长度 fseek(filePTR, 0, SEEK_SET); // 回到文件开头 char *buffer = malloc(length + 1); // 崩溃发生在此处 if (buffer == NULL) { printf("Error creating buffer\n"); exit(1); } int currentChar = 0; do { if (feof(filePTR)) { break; } buffer[currentChar] = fgetc(filePTR); currentChar++; } while (1); fclose(filePTR); buffer[currentChar - 1] = '\0'; return buffer; }
项目编译命令
emcc --no-entry -s INITIAL_MEMORY=128mb -Wl,--export=__heap_base -Wl,--export=__data_end -Wl,--export=malloc -Wl,--export=free -sENVIRONMENT=node --profiling -sRUNTIME_DEBUG=1 -fsanitize=undefined -sLLD_REPORT_UNDEFINED -g3 -sSTACK_OVERFLOW_CHECK=2 -sASSERTIONS=2 src/Main.c src/C/cJSON/cJSON.c src/DependencyGraph/DependencyGraph.c ./src/C/StringRelatedFunctions.c ./src/Regex/RegexFunctions.c ./src/DependencyGraph/FindDependencies.c ./src/SettingsSingleton/settingsSingleton.c ./src/C/ProblemHandler.c ./src/C/TextColors.c ./src/C/FileHandler.c ./src/C/IntFunctions.c ./src/Minifiers/HTMLMinifier.c ./src/C/FileTypesHandler.c ./src/C/Stack.c ./src/C/BundleFiles.c ./src/C/ProgressBar.c ./src/C/StringShiftHandler.c ./src/Minifiers/JSMinifier.c -s EXPORT_ES6=0 -s MODULARIZE -s USE_ES6_IMPORT_META=0 -s EXPORTED_RUNTIME_METHODS=["ccall"] -s NODERAWFS=1 -sBINARYEN=1 -sEXIT_RUNTIME=1 -sALLOW_MEMORY_GROWTH -o Build/CFunctions.js
WebAssembly初始化JavaScript代码
#!/usr/bin/env node "use strict"; // js wrapper for arrowpack for NPM const fs = require("fs"); const path = require("path"); const chalk = require("chalk"); const settingsSingleton = require("./SettingsSingleton/settingsSingleton"); const DirFunctions = require("./js/DirFunctions"); const wasm_exec = require("../Build/wasm_exec.js"); const CFunctionFactory = require("../Build/CFunctions.js"); const go = new Go(); // const Sleep = require("../src/js/Sleep"); const { mkdirIfNotExists } = require("./js/DirFunctions.js"); var StartTime = performance.now(); const argv = require("yargs/yargs")(process.argv.slice(2)) .option("c", { alias: "config-path", describe: "Path to config file if not in working directory", type: "string" }) .help().argv; var CONFIG_FILE_NAME = "ArrowPack-config.json" if (argv.c) { CONFIG_FILE_NAME = path.join(argv.c, CONFIG_FILE_NAME) } else { console.log("no custom file thingy"); } var rawconfigData = null; if (fs.existsSync(CONFIG_FILE_NAME)) { rawconfigData = fs.readFileSync(CONFIG_FILE_NAME, "utf8"); } var temp; const Settings = new settingsSingleton(rawconfigData); if (Settings.getValue("largeProject") === false) { temp = DirFunctions.RecursiveWalkDir(Settings.getValue("entry")); // eventually add pluginAPI event here } else { let RecursiveWalkDirWASM = fs.readFileSync("../Build/RecursiveWalkDir.wasm"); const { WebAssembly } = require("wasi"); let compiledWalkDirWASM = WebAssembly.compile(wasm); let InstanceWalkDirWASM = WebAssembly.instantiate(compiledWalkDirWASM); const { InstanceWalkDirWASMExports } = instance; temp = InstanceWalkDirWASMExports.walk_dir(Settings.getValue("entry")); } var WalkedFiles = temp.Files; var WalkedDirs = temp.Directories; console.log(WalkedDirs); if (WalkedDirs) { WalkedDirs.forEach(Dir => { console.log(chalk.red(Dir)); var tempDir = Settings.getValue("exit") + Dir.substring(Settings.getValue("entry").length); console.log(chalk.yellowBright(tempDir)); DirFunctions.mkdirIfNotExists(tempDir); //DirFunctions.mkdirIfNotExists(Dir); }); } DirFunctions.mkdirIfNotExists("ARROWPACK_TEMP_PREPROCESS_DIR"); var AbsoluteFilesCharLength = 0; var WrappedWalkedFiles = ""; if (WalkedFiles && WalkedFiles.length > 0) { // Paths are wrapped into one string because passing array of strings from JS to C is complicated WalkedFiles.forEach(FilePath => { WrappedWalkedFiles += FilePath + "::"; console.log(chalk.bold.blue(FilePath)); AbsoluteFilesCharLength += FilePath.length; }); console.log(chalk.red(WalkedFiles.length)); var StructsPointer; CFunctionFactory().then((CFunctions) => { CFunctions._CheckWasm(); CFunctions._InitFileTypes(); for (let k in Settings.settings) { if (CFunctions.ccall( "SendSettingsString", "number", ["string"], [k] ) != 1) { throw "Error sending Wasm settings string: " + k; } console.log(chalk.bold.blue(k)); // Gives time to apply settings if (CFunctions.ccall( "SendSettingsString", "number", ["string"], [Settings.settings[k].toString()] ) != 1) { throw "Error sending Wasm settings string: " + Settings.settings[k]; } console.log(chalk.bold.blue(Settings.settings[k])); // Also gives time to apply settings } var Success; // StructsPointer = CFunctions._CreateTree(allocateUTF8(WrappedWalkedFiles), WalkedFiles.length, AbsoluteFilesCharLength); // Need to get this working eventually for faster speed but couldn't work out allocateUTF8 StructsPointer = CFunctions.ccall( "CreateGraph", "number", ["string", "number"], [WrappedWalkedFiles, WalkedFiles.length] ); Success = CFunctions.ccall( "BundleFiles", "number", ["number"], [StructsPointer] ); if (Success === 1 || Success === 0) { fs.rm("ARROWPACK_TEMP_PREPROCESS_DIR", { recursive: true }, (err) => { if (err) { console.error(err); } else { console.log("Sucessfully removed temporary preprocess directory"); } }); DirFunctions.DeleteDirectory(); //CFunctions.ccall("PrintTimeTaken", "void", ["number", "number"], [StartTime, performance.now()]); // Not working for some reason console.log(chalk.magentaBright("Bundling files completed in " + (performance.now() - StartTime) / 1000 + " seconds")); } }); }
排查方向与解决方案
校验文件长度有效性
ftell可能因文件无法seek、权限问题返回-1,此时length + 1会溢出为超大负数,导致malloc请求非法内存。添加前置检查:if (length == -1) { fclose(filePTR); printf("Failed to get file length for %s\n", path); return NULL; }- 文本模式
"r"会转换换行符,导致ftell返回值与实际读取字节数不一致,改用二进制模式"rb"打开文件避免转换。
修复读取循环逻辑
- 当前循环在
fgetc返回EOF后仍会执行一次赋值,导致currentChar越界。修改为:int currentChar = 0; int ch; while ((ch = fgetc(filePTR)) != EOF && currentChar < length) { buffer[currentChar++] = ch; } buffer[currentChar] = '\0'; - 添加
currentChar < length限制,防止文件读取过程中被修改导致长度超出预期。
- 当前循环在
启用内存调试工具
- 添加编译参数
-s MALLOC_DEBUG=1,启用dlmalloc调试模式,输出详细内存错误信息。 - 替换
-fsanitize=undefined为-fsanitize=address,利用AddressSanitizer检测堆越界、重复free等内存破坏问题。
- 添加编译参数
调整WebAssembly内存配置
- 尝试增大初始内存,比如
-s INITIAL_MEMORY=256mb,同时确认-s ALLOW_MEMORY_GROWTH正常工作,避免内存耗尽。
- 尝试增大初始内存,比如
检查JS与C交互的内存管理
- 确保所有从C返回的内存(如
ReadDataFromFile的buffer)在JS中调用CFunctions._free释放,避免内存泄漏或堆空间污染。
- 确保所有从C返回的内存(如
内容的提问来源于stack exchange,提问作者redstarbird
相关产品推荐
相关产品推荐

