簡體   English   中英

使用 MinGW 編譯的 Node.js (node-api) addon 導致訪問沖突

[英]Node js (node-api) addon compiled with MinGW causes access violation

構建 node-api 鏈接的本機插件

經過3天的調查和研究,我對問題的原因一無所知。 基本上我正在加載一個用 MinGW64 編譯並鏈接到 C node-api 的 hello world Node JS 插件。

代碼如下:

// hello.c

#include <node/node_api.h>

napi_value Method(napi_env env, napi_callback_info args)
{
    napi_value greeting;
    napi_status status = napi_create_string_utf8(env, "hello, asshole", NAPI_AUTO_LENGTH, &greeting);

    return status == napi_ok ? greeting : (napi_value)0;
}

napi_value init(napi_env env, napi_value exports)
{
    napi_value function;

    napi_status status = napi_create_function(env, 0, 0, &Method, 0, &function);
    if (status != napi_ok)
        return (napi_value)0;

    status = napi_set_named_property(env, exports, "hello", function);
    return status == napi_ok ? exports : (napi_value)0;
}

// static void _register_hello(void)__attribute((constructor));
// calls napi_module_register
NAPI_MODULE(hello, init)

我已經下載了我鏈接的 dist 頭文件和預編譯node.lib 這是我的CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.11.4 FATAL_ERROR)

project(hello-node-api LANGUAGES C CXX)

add_library(hello SHARED hello.c)

# TODO: download dist and make an imported target
target_include_directories(hello PRIVATE node-v16.2.0/include)
target_link_libraries(hello PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/node-v16.2.0/node.lib)

set_target_properties(hello PROPERTIES
        SUFFIX ".node"
        PREFIX ""
        )

target_compile_definitions(hello PUBLIC BUILDING_NODE_EXTENSION)

關於插件加載例程的一些解釋:

Node.exe 是使用node.lib中提供的導出符號編譯的。
Node.exe 加載一個插件並調用 dll 入口點(在 Windows 上是__DllMainCRTStart )。
dll 版本(使用 MSVC 或 MinGW 編譯)中的任何一個都可以正常加載:調用入口點,並且可以無錯誤地調用所有用戶定義的函數。 但是嘗試調用導入的napi_會導致與 MinGW 的訪問沖突。

Exception thrown at 0x0000000000009238 in node.exe: 0xC0000005: Access violation executing location 0x0000000000009238.

使用 MSVC api 功能可以正常輸入並且插件可以注冊等。

加載 MinGW 插件的模擬虛擬 MSVC c++ exe 鏈接到從 exe 導出的 C MSVC 符號:

首先,我認為問題是由編譯器引起的,但是無論用於構建 exe 的編譯器如何,虛擬 exe(MSVC)->addon(MinGW)->exe_symbols(MSVC) 都可以正常工作:使用extern "C"導出符號:

出口header:

// esport.h

#pragma once

#ifndef BUILDING
#define EXPORT_API __declspec(dllimport)
#else
#define EXPORT_API __declspec(dllexport)
#endif

EXPORT_API void hello_addon();
EXPORT_API int sum(int a, int b);

添加在:

// addon.c
#include <export.h>

static void print_hello_from_export(void)__attribute((constructor));

void print_hello_from_export(void)
{
    int res = sum(4, 15);
    hello_addon();
}

可執行程序:

extern "C" {
#include "export.h"
}

#include <iostream>

void hello_addon()
{
    std::cout << "hello addon" << std::endl;
}

int sum(int a, int b)
{
    return a + b;
}

#include <windows.h>

int main()
{
    HMODULE handle = LoadLibraryA("addon.dll");
    return 0;
}

因此,無論用於構建可執行文件的編譯器如何,都會加載庫並打印一條消息。 使用 C ABI 時會出現此行為。

回到node-api的問題

我試圖查看hello.node二進制文件中的符號,但我不知道如何處理這些信息。

...
[894](sec  1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x0000000000001410 __CTOR_LIST__
[895](sec  8)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x0000000000000014 _head_lib64_libkernel32_a
[896](sec  8)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x0000000000000150 __imp_napi_module_register
...

下面是 cmake 生成的 Ninja 腳本:MSVC build

build CMakeFiles\hello.dir\hello.c.obj: C_COMPILER__hello_Debug C$:\dev\repos\hello-node-api\hello.c || cmake_object_order_depends_target_hello
  DEFINES = -DBUILDING_NODE_EXTENSION -Dhello_EXPORTS
  FLAGS = /DWIN32 /D_WINDOWS /W3 /MDd /Zi /Ob0 /Od /RTC1
  INCLUDES = -IC:\dev\repos\hello-node-api\node-v16.2.0\include
  OBJECT_DIR = CMakeFiles\hello.dir
  OBJECT_FILE_DIR = CMakeFiles\hello.dir
  TARGET_COMPILE_PDB = CMakeFiles\hello.dir\
  TARGET_PDB = hello.pdb


# =============================================================================
# Link build statements for SHARED_LIBRARY target hello


#############################################
# Link the shared library hello.node

build hello.node hello.lib: C_SHARED_LIBRARY_LINKER__hello_Debug CMakeFiles\hello.dir\hello.c.obj | C$:\dev\repos\hello-node-api\node-v16.2.0\node.lib
  LANGUAGE_COMPILE_FLAGS = /DWIN32 /D_WINDOWS /W3 /MDd /Zi /Ob0 /Od /RTC1
  LINK_FLAGS = /machine:x64 /debug /INCREMENTAL
  LINK_LIBRARIES = C:\dev\repos\hello-node-api\node-v16.2.0\node.lib  kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib
  OBJECT_DIR = CMakeFiles\hello.dir
  POST_BUILD = cd .
  PRE_LINK = cd .
  RESTAT = 1
  TARGET_COMPILE_PDB = CMakeFiles\hello.dir\
  TARGET_FILE = hello.node
  TARGET_IMPLIB = hello.lib
  TARGET_PDB = hello.pdb

MinGW 構建

#############################################
# Order-only phony target for hello

build cmake_object_order_depends_target_hello: phony || CMakeFiles/hello.dir

build CMakeFiles/hello.dir/hello.c.obj: C_COMPILER__hello_Debug C$:/dev/repos/hello-node-api/hello.c || cmake_object_order_depends_target_hello
  DEFINES = -DBUILDING_NODE_EXTENSION -Dhello_EXPORTS
  DEP_FILE = CMakeFiles\hello.dir\hello.c.obj.d
  FLAGS = -g
  INCLUDES = -IC:/dev/repos/hello-node-api/node-v16.2.0/include
  OBJECT_DIR = CMakeFiles\hello.dir
  OBJECT_FILE_DIR = CMakeFiles\hello.dir


# =============================================================================
# Link build statements for SHARED_LIBRARY target hello


#############################################
# Link the shared library hello.node

build hello.node libhello.dll.a: C_SHARED_LIBRARY_LINKER__hello_Debug CMakeFiles/hello.dir/hello.c.obj | C$:/dev/repos/hello-node-api/node-v16.2.0/node.lib
  LANGUAGE_COMPILE_FLAGS = -g
  LINK_LIBRARIES = C:/dev/repos/hello-node-api/node-v16.2.0/node.lib  -lkernel32 -luser32 -lgdi32 -lwinspool -lshell32 -lole32 -loleaut32 -luuid -lcomdlg32 -ladvapi32
  OBJECT_DIR = CMakeFiles\hello.dir
  POST_BUILD = cd .
  PRE_LINK = cd .
  RESTAT = 1
  TARGET_FILE = hello.node
  TARGET_IMPLIB = libhello.dll.a
  TARGET_PDB = hello.node.dbg

我無法發現編譯和鏈接方面的任何顯着差異。

我還注意到為構建 Node 生成的 visual studio 項目有一個獨特的目標來構建node.lib 我不知道這是否重要,但這是.vsproj的命令行參數

/Yu"node_pch.h" /MP /GS /W3 /wd"4351" /wd"4355" /wd"4800" /wd"4251" /wd"4275" /wd"4267" /Zc:wchar_t /I"src" /I"out\Debug\obj\global_intermediate" /I"out\Debug\obj\global_intermediate\include" /I"out\Debug\obj\global_intermediate\src" /I"tools\msvs\genfiles" /I"deps\histogram\src" /I"deps\uvwasi\include" /I"deps\v8\include" /I"deps\icu-small\source\i18n" /I"deps\icu-small\source\common" /I"deps\zlib" /I"deps\llhttp\include" /I"deps\cares\include" /I"deps\uv\include" /I"deps\nghttp2\lib\includes" /I"deps\brotli\c\include" /I"deps\openssl\openssl\include" /I"deps\ngtcp2" /I"deps\ngtcp2\ngtcp2\lib\includes" /I"deps\ngtcp2\ngtcp2\crypto\includes" /I"deps\ngtcp2\nghttp3\lib\includes" /Z7 /Gm- /Od /Fd"out\Debug\obj\libnode\libnode.pdb" /FI"node_pch.h" /Zc:inline /fp:precise /D "V8_DEPRECATION_WARNINGS" /D "V8_IMMINENT_DEPRECATION_WARNINGS" /D "_GLIBCXX_USE_CXX11_ABI=1" /D "WIN32" /D "_CRT_SECURE_NO_DEPRECATE" /D "_CRT_NONSTDC_NO_DEPRECATE" /D "_HAS_EXCEPTIONS=0" /D "BUILDING_V8_SHARED=1" /D "BUILDING_UV_SHARED=1" /D "OPENSSL_NO_PINSHARED" /D "OPENSSL_THREADS" /D "OPENSSL_NO_ASM" /D "NODE_ARCH=\"x64\"" /D "NODE_WANT_INTERNALS=1" /D "V8_DEPRECATION_WARNINGS=1" /D "NODE_OPENSSL_SYSTEM_CERT_PATH=\"\"" /D "HAVE_INSPECTOR=1" /D "HAVE_ETW=1" /D "FD_SETSIZE=1024" /D "NODE_PLATFORM=\"win32\"" /D "NOMINMAX" /D "_UNICODE=1" /D "NODE_USE_V8_PLATFORM=1" /D "NODE_HAVE_I18N_SUPPORT=1" /D "HAVE_OPENSSL=1" /D "UCONFIG_NO_SERVICE=1" /D "U_ENABLE_DYLOAD=0" /D "U_STATIC_IMPLEMENTATION=1" /D "U_HAVE_STD_STRING=1" /D "UCONFIG_NO_BREAK_ITERATION=0" /D "NGHTTP2_STATICLIB" /D "NGTCP2_STATICLIB" /D "NGHTTP3_STATICLIB" /D "DEBUG" /D "_DEBUG" /D "V8_ENABLE_CHECKS" /errorReport:prompt /GF /WX- /Zc:forScope /RTC1 /Gd /Oy- /MTd /FC /Fa"out\Debug\obj\libnode\" /nologo /Fo"out\Debug\obj\libnode\" /Fp"out\Debug\obj\libnode\libnode.pch" /diagnostics:column 

我沒有想法,在 web 上有一些沒有結果的嘗試加載 mingw 編譯的插件。 其中一些是幾年前的,但仍然沒有結果。 所以我向社區尋求幫助以解決這個問題或者至少理解為什么它無法解決。

所以它歸結為:

  1. 是什么導致訪問沖突? 是因為導入的符號地址不在地址空間內(我該如何檢查?),或者鏈接期間使用的符號地址與node.exe中的地址不對應?
  2. 如果代插件無法解決問題,可能是Node編譯出了什么問題?

檢查function地址:

我決定調查加載的(到底是誰加載的?是__DllMainRTCStartup嗎?)地址與node.exe中的地址不同的可能性

node.exe內部: 0x00007ff629d198e0 {node.exe!napi_module_register(napi_module *)}

拆卸:

// Registers a NAPI module.
void napi_module_register(napi_module* mod) {
00007FF629D198E0  mov         qword ptr [rsp+8],rcx  
00007FF629D198E5  push        rsi  
00007FF629D198E6  push        rdi  
00007FF629D198E7  sub         rsp,0C8h  
00007FF629D198EE  mov         rdi,rsp  
00007FF629D198F1  mov         ecx,32h  
00007FF629D198F6  mov         eax,0CCCCCCCCh  
00007FF629D198FB  rep stos    dword ptr [rdi]  
00007FF629D198FD  mov         rcx,qword ptr [mod]   
...

但是當在hello.node內部時: identifier "napi_module_register" is undefined - 我不知道它是否是預期的。 拆卸:

static void _register_hello(void) __attribute((constructor));
static void _register_hello(void)
{
00007FFC02F81479  push        rbp  
00007FFC02F8147A  mov         rbp,rsp  
00007FFC02F8147D  sub         rsp,20h  
    napi_module_register(&_module);
00007FFC02F81481  lea         rcx,[7FFC02F83020h]  
00007FFC02F81488  mov         rax,qword ptr [7FFC02F89150h]  
00007FFC02F8148F  call        rax  
}
00007FFC02F81491  nop  
00007FFC02F81492  add         rsp,20h  
00007FFC02F81496  pop         rbp  
00007FFC02F81497  ret  

call rax導致0000000000009238?? ?????? 0000000000009238?? ?????? 然后拋出訪問沖突。


看起來node.exe的導入表是空的:

There is an import table in .idata at 0xb32a9000

The Import Tables (interpreted .idata section contents)
 vma:            Hint    Time      Forward  DLL       First
                 Table   Stamp     Chain    Name      Thunk
 00009000   00009084 00000000 00000000 000092a0 00009170

    DLL Name: node.exe
    vma:  Hint/Ord Member-Name Bound-To

對於使用 MSVC 編譯的 dll,它不是空的:

Dump of file .\hello.node

File Type: DLL

  Section contains the following imports:

    node.exe
             18000E1F8 Import Address Table
             18000E5E8 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                        6707 napi_set_named_property
                        66A4 napi_create_function
                        66F3 napi_module_register
                        66AD napi_create_string_utf8

看起來它可能畢竟是原因。

一個空的導入地址表可能是原因嗎?

我相信你的問題是這樣的:

LINK_LIBRARIES = C:/dev/repos/hello-node-api/node-v16.2.0/node.lib

node.lib 是一個 MSVC 庫,不適用於 mingw。

您需要創建一個 .def 文件(它是一個帶有 napi 符號的文本文件):

LIBRARY node.exe
EXPORTS
napi_set_named_property
napi_create_function
etc

每個 napi 版本的導出列表在這個 repo 中: https://github.com/nodejs/node-api-headers

最后用mingw的dlltool把.def轉成aa(靜態庫)。 將該 lib 文件與您的 mingw 程序鏈接起來。

這是一個合法的解決方法,它提供了一些“臨時”解決方案。 它涉及從調用應用程序顯式加載符號,該應用程序必須是node.exe

盡管它確實有效,但我仍在尋找一種傳統的解決方案來正確地隱式加載導入的符號。


#include <node/node_api.h>

#include <windows.h>

static void (*pRegisterModule)(napi_module*);
static napi_status (*pCreateStringUtf8)(napi_env, const char*, size_t, napi_value *);
static napi_status (*pCreateFunction)(napi_env, const char*, size_t, napi_callback, void*, napi_value*);
static napi_status (*pSetNamedProperty)(napi_env, napi_value, const char*, napi_value);

napi_value Method(napi_env env, napi_callback_info args)
{
    napi_value greeting;
    napi_status status = // napi_create_string_utf8(env, "hello workaround", NAPI_AUTO_LENGTH, &greeting);
            pCreateStringUtf8(env, "hello workaround", NAPI_AUTO_LENGTH, &greeting);

    return status == napi_ok ? greeting : (napi_value)0;
}

napi_value init(napi_env env, napi_value exports)
{
    napi_value function;

    napi_status status = // napi_create_function(env, 0, 0, &Method, 0, &function);
            pCreateFunction(env, 0, 0, &Method, 0, &function);

    if (status != napi_ok)
        return (napi_value)0;

    status = // napi_set_named_property(env, exports, "hello", function);
            pSetNamedProperty(env, exports, "hello", function);
    return status == napi_ok ? exports : (napi_value)0;
}

// static void _register_hello(void)__attribute((constructor));
// calls napi_module_register
//NAPI_MODULE(hello, init)
static napi_module _module = {NAPI_MODULE_VERSION,
                           0,
                           __FILE__,
                           init,
                           "hello",
                           (void*)0,
                           {0}};

typedef void(WINAPI *void_func_ptr_t)(void);

static void _register_hello(void) __attribute((constructor));
static void _register_hello(void)
{
    // TODO: load pointers from calling exe
    *(void**)&pRegisterModule = GetProcAddress(GetModuleHandleW(0),
                                  "napi_module_register");
    *(void**)&pCreateStringUtf8 = GetProcAddress(GetModuleHandleW(0),
                                                 "napi_create_string_utf8");
    *(void**)&pCreateFunction = GetProcAddress(GetModuleHandleW(0),
                                                 "napi_create_function");
    *(void**)&pSetNamedProperty = GetProcAddress(GetModuleHandleW(0),
                                               "napi_set_named_property");

    pRegisterModule(&_module);
//    napi_module_register(&_module);
}


暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM