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

1

Building node-api linked native addon

I am out of thoughts on the cause of the problem after 3 days of investigating and researching. Basically I am loading a hello world Node JS addon compiled with MinGW64 and linked against C node-api.

The code is as follows:

// 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)

I have downloaded dist headers and precompiled node.lib which I link against. This is my CMakeLists.txt file:

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)

Some explanation on the addon loading routine:

Node.exe is compiled with exported symbols, that are provided within the node.lib.
Node.exe loads an addon and a dll entry point (on Windows it is __DllMainCRTStart) is called.
Either of the dll versions (compiled either with MSVC or MinGW) are loaded just fine: the entry point is called, and all the user-defined functions are callable without errors. But an attempt to invoke an imported napi_ function results in access violation with MinGW.

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

With MSVC api functions are entered normally and the addon gets registered etc.

Analog dummy MSVC c++ exe that loads a MinGW addon that links to exported C MSVC symbols from exe:

First I thought that the problem is caused by the compiler, but a dummy exe(MSVC)->addon(MinGW)->exe_symbols(MSVC) works just fine regardless of the compiler used to build an exe: extern "C" is used to export symbols:

Export 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:

// 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();
}

exe:

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;
}

So, the library is loaded and a message is printed, regardless of the compiler used to build the executable. This behavior is expected when using the C ABI.

Back to the problem with node-api

I tried to look through the symbols within the hello.node binary but I don't know what to do with this info.

...
[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
...

Here is the Ninja script generated by cmake: 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 build

#############################################
# 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

I can't spot any significant differences in compilation and linking.

I also noticed that visual studio project generated to build Node has a distinct target to build node.lib. I don't know if it matters, but here is the command line args from .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 

I am out of ideas, on the web there are some fruitless attempts to load a mingw-compiled addon. Some of them are from several years ago and still no result. So I ask the community for help in order to solve this issue or at least understand why it can't be solved.

So it boils down to:

  1. What causes the access violation? Is it because of the imported symbol address is not within the address space (how can I check it?), or maybe the address of the symbol that was used during linking does not correspond to the address within the node.exe?
  2. If the problem can't be solved on behalf of the addon, what might be the problem in Node compilation?

Checking function addresses:

I decided to look into the possibility that the loaded (who exactly loads it? Is it __DllMainRTCStartup?) addresses differ from the addresses within the node.exe

While inside the node.exe: 0x00007ff629d198e0 {node.exe!napi_module_register(napi_module *)}

Disassembly:

// 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]   
...

But when inside the hello.node: identifier "napi_module_register" is undefined - I don't know if it is expected. Disassembly:

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 leads to 0000000000009238 ?? ?????? and then access violation is thrown.


It looks like the import table for node.exe is empty:

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

And for dll compiled with MSVC it is NOT EMPTY:

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

Looks like it mught be the reasin after all.

Can an empty import address table be the reason?

c++
node.js
dll
access-violation
nodeapi
asked on Stack Overflow May 22, 2021 by Sergey Kolesnik • edited May 23, 2021 by Sergey Kolesnik

1 Answer

0

This is a legit workaround, that provides some "temporary" solution. It involves explicit loading of symbols from the calling application, which has to be node.exe.

Though it does work, I am still looking for a conventional solution to correctly load imported symbols implicitly.


#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);
}


answered on Stack Overflow May 22, 2021 by Sergey Kolesnik

User contributions licensed under CC BY-SA 3.0