Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make better dependency management #142

Open
MolotovCherry opened this issue Oct 10, 2023 · 11 comments
Open

Make better dependency management #142

MolotovCherry opened this issue Oct 10, 2023 · 11 comments

Comments

@MolotovCherry
Copy link
Contributor

MolotovCherry commented Oct 10, 2023

I just want to contribute here, (don't want to go through a PR rn), you can automate building the dependency in Rust using the cmake build dependency, and this build.rs. (Also, clone libmem repo and its submodules to a subfolder in your project)

Feel free to take this code anyone. Specifically, it's for Windows and a static build. Enjoy 😄

Code
use std::error::Error;
use std::path::PathBuf;

use winreg::enums::HKEY_LOCAL_MACHINE;
use winreg::RegKey;

pub fn get_windows_kits_dir() -> Result<PathBuf, Box<dyn Error>> {
    let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
    let key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
    let dir: String = hklm.open_subkey(key)?.get_value("KitsRoot10")?;

    Ok(dir.into())
}

/// Retrieves the path to the user mode libraries. The path may look something like:
/// `C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\um`.
pub fn get_um_dir() -> Result<PathBuf, Box<dyn Error>> {
    // We first append lib to the path and read the directory..
    let dir = get_windows_kits_dir()?.join("Lib").read_dir()?;

    // In the lib directory we may have one or more directories named after the version of Windows,
    // we will be looking for the highest version number.
    let mut dir = dir
        .filter_map(Result::ok)
        .map(|dir| dir.path())
        .filter(|dir| {
            dir.components()
                .last()
                .and_then(|c| c.as_os_str().to_str())
                .map_or(false, |c| c.starts_with("10.") && dir.join("um").is_dir())
        })
        .max()
        .ok_or_else(|| "not found")?;

    dir.push("um");
    dir.push("x64");

    // Finally append um to the path to get the path to the user mode libraries.
    Ok(dir)
}

fn main() {
    // build and link libmem
    //
    // build times are long! it is recommended to cache these instead, and take the build artifacts generated
    // and hardcode this buildscript to your generated .lib file

    let mut config = cmake::Config::new("libmem");

    config.generator("NMake Makefiles");
    config.define("LIBMEM_BUILD_TESTS", "OFF");
    config.define("LIBMEM_BUILD_STATIC", "ON");
    // Build erorrs out in debug mode, recommended to cache artifacts
    config.define("CMAKE_BUILD_TYPE", "Release");
    config.build_target("libmem");

    let dst = config.build();

    let build_path = dst.join("build");

    // libmem.lib, llvm.lib
    println!("cargo:rustc-link-search=native={}", build_path.display());
    // keystone.lib
    println!(
        r"cargo:rustc-link-search=native={}\keystone-engine-prefix\src\keystone-engine-build\llvm\lib",
        build_path.display()
    );
    // capstone.lib
    println!(
        r"cargo:rustc-link-search=native={}\capstone-engine-prefix\src\capstone-engine-build",
        build_path.display()
    );
    // LIEF.lib
    println!(
        r"cargo:rustc-link-search=native={}\lief-project-prefix\src\lief-project-build",
        build_path.display()
    );
    println!("cargo:rustc-link-lib=static=keystone");
    println!("cargo:rustc-link-lib=static=capstone");
    println!("cargo:rustc-link-lib=static=LIEF");
    println!("cargo:rustc-link-lib=static=llvm");
    println!("cargo:rustc-link-lib=static=libmem");

    // user32.lib, psapi.lib, ntdll.lib
    println!(
        "cargo:rustc-link-search=native={}",
        get_um_dir().unwrap().display()
    );

    println!("cargo:rustc-link-lib=static=user32");
    println!("cargo:rustc-link-lib=static=psapi");
    println!("cargo:rustc-link-lib=static=ntdll");
}
@rdbo
Copy link
Owner

rdbo commented Oct 11, 2023

I actually thought about that for a while (and I had implemented it even), but I decided not to implement it simply because you can't ship the whole repository to crates.io (too big) and I wouldn't want the build to be reliable on GitHub.
An alternative to this could be a script that downloads pre-built binaries for every platform and puts them in the correct path. But I have to take time to make binary builds for every single platform.

@MolotovCherry
Copy link
Contributor Author

MolotovCherry commented Oct 11, 2023

I completely understand the reasons. I still think that having a ready to go build.rs file in the docs at least with some steps would be really helpful (and thankfully, this build.rs won't mess with the one already in the crate on crates.io)

E.g.

  1. Clone libmem repo to a subfolder in your project with git clone --recurse-submodules --remote-submodules <YourGitHubUrl>
  2. Drop this build.rs script in your project to build it. If you want, you can implement additional functionality to save/cache the generated libs and hardcode the path to them so they don't recompile all the time

Or you can go about it this other way with these other steps, and install it globally to make it easy on yourself later, with this other build script which is simple and hardcoded

(One bonus the former one has is when it comes to github actions builds, which can also be cached, though I should note that github actions will fail to build this unless you set CARGO_TARGET_DIR to something like D:\target, since cmake runs out of path space on the build otherwise)

(I got this building on github actions, and it's quite convenient)

By the way, thanks for your library and the hard work you put into it! It's really nice! 😄

@rdbo rdbo changed the title Build dependency with build script in Rust Make better dependency management Oct 11, 2023
@rdbo
Copy link
Owner

rdbo commented Oct 11, 2023

I'm working on other projects right now, but I see that this is definitely something to worry about. I'll pin this issue so (hopefully) I don't forget. Thanks for your suggestion 👍

@rdbo rdbo pinned this issue Oct 11, 2023
@nathan818fr
Copy link
Contributor

nathan818fr commented Dec 4, 2023

About pre-built binaries, I've made a repo that creates them for multiple platforms (the script can be used standalone or via GitHub Actions to create GitHub Releases): https://github.com/nathan818fr/libmem-build
Feel free to fork or use parts of this repo.

There is one difference regarding linux: it provide the static libraries independently (so it use liblibmem_partial.a instead of the one created by makebundle.sh). This makes the artifacts more similar between Windows and Linux.

PS: Thanks for libmem

@rdbo
Copy link
Owner

rdbo commented Dec 4, 2023

@nathan818fr
That's impressive! We could definitely use that here 💯
For the static library, I think one might have linking issues with it. If I remember correctly, the linker does not link multiple static libraries together into a single one, so the third party dependencies will be left off. And that's what makebundle.sh is supposed to solve (it generates a big binary though).
With that said, I will be taking a better look at the repo, and PRs are welcome if you wish to contribute directly
Having that implemented in the main repo will save a lot of time here, thanks for your efforts 👍

@nathan818fr
Copy link
Contributor

nathan818fr commented Dec 5, 2023

Yes, with my way, all libraries must be linked to the target program.
That's what I'm doing here on a project using libmem: https://github.com/nathan818fr/ts3-server-hook/blob/9ed27aaea3b4edb15fe9a984c0d5709d9e1f99b6/cmake/libmem-config.cmake

It's true that it's probably simpler to distribute just one big static binary (probably easier for users, etc.).
In that case, would it be nice to do the same thing on Windows (so it would give a similar result between the different platforms with only: liblibmem.so|libmem.dll and liblibmen.a|libmem.lib)? I'll take a look.

Are you OK with the current artifact structure?

  • include/libmem/ - directory with libmem headers
  • lib/ - directory with libmem dynamic and static libraries
  • licenses/ - directory containing all licenses (libmem and dependencies)
  • GLIBC_VERSION.txt|MUSL_VERSION.txt|MSVC_VERSION.txt|[...] - single-line text file providing essential version information for the platform (e.g. the glibc version used at compile-time generally indicates the minimum version required by the runtime - that's why I compile on debian buster, the oldoldstable)

I will submit a PR soon.

@rdbo
Copy link
Owner

rdbo commented Dec 5, 2023

@nathan818fr IIRC, MSVC is able to ship all the libraries into one without any additional steps, so that's why the makebundle.sh script doesn't run for Windows.
As for the structure, I think it's pretty good. Everything important seems to be included 💯

@nathan818fr
Copy link
Contributor

nathan818fr commented Dec 5, 2023

Maybe I'm missing something, but when I build with MSVC I don't get all shipped into one library. I get:

  • libmem.lib (946 KiB)
  • injector.lib (76 KiB)
  • capstone.lib (2 426 KiB)
  • keystone.lib (3 5801 KiB)
  • LIEF.lib (172 023 KiB)
  • llvm.lib (4 156 KiB)

And if I try to compile a demo that uses LM_HookCode, it doesn't work without explicitly including the other libraries.

libmem build logs
git clone --recurse-submodules https://github.com/rdbo/libmem.git

cmake -S . -B build-static -DLIBMEM_BUILD_STATIC=ON
cmake --build build-static


**********************************************************************
** Visual Studio 2022 Developer PowerShell v17.8.1
** Copyright (c) 2022 Microsoft Corporation
**********************************************************************
PS C:\Users\user\Tmp> git clone --recurse-submodules https://github.com/rdbo/libmem.git
[...]
PS C:\Users\user\Tmp> cd libmem
PS C:\Users\user\Tmp\libmem> cmake -S . -B build-static -DLIBMEM_BUILD_STATIC=ON
-- The C compiler identification is MSVC 19.38.33130.0
-- The CXX compiler identification is MSVC 19.38.33130.0
-- The ASM compiler identification is MSVC
-- Found assembler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.38.33130/bin/Hostx64/x64/cl.exe
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.38.33130/bin/Hostx64/x64/cl.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.38.33130/bin/Hostx64/x64/cl.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- [*] Platform: Windows
-- [*] Build tests: OFF
-- [*] Build static library: ON
-- [*] Build for 32 bits: OFF
-- CMAKE_C_FLAGS: /DWIN32 /D_WINDOWS
-- CMAKE_CXX_FLAGS: /DWIN32 /D_WINDOWS /EHsc
-- Configuring done (4.2s)
-- Generating done (0.1s)
-- Build files have been written to: C:/Users/user/Tmp/libmem/build-static
PS C:\Users\user\Tmp\libmem> cmake --build build-static
[...]
[100%] Linking CXX static library libmem.lib
[100%] Built target libmem
PS C:\Users\user\Tmp\libmem> dir -s build-static "*.lib"

    Directory: C:\Users\user\Tmp\libmem\build-static

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        05/12/2023     10:10         968908 libmem.lib

    Directory: C:\Users\user\Tmp\libmem\build-static\external

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        05/12/2023     10:10          77874 injector.lib


    Directory: C:\Users\user\Tmp\libmem\build-static\external\capstone-engine-prefix\src\capstone-engine-build

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        05/12/2023     10:06        2483872 capstone.lib

    Directory: C:\Users\user\Tmp\libmem\build-static\external\keystone-engine-prefix\src\keystone-engine-build\llvm\lib

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        05/12/2023     10:07       36660216 keystone.lib

    Directory: C:\Users\user\Tmp\libmem\build-static\external\lief-project-prefix\src\lief-project-build

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        05/12/2023     10:10      176151074 LIEF.lib

    Directory: C:\Users\user\Tmp\libmem\build-static\external\llvm

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        05/12/2023     10:10        4255850 llvm.lib
test compile logs (non-working + working)

demo.c

#include <libmem/libmem.h>
#pragma comment(lib, "shell32.lib")

void a() { printf("a\n"); }
void b() { printf("b\n"); }

int main(int argc, char **argv) {
  LM_HookCode((lm_address_t) &a, (lm_address_t) &b, 0);
  a();
  return 0;
}

It don't compile when I only link libmem.lib:

PS C:\Users\user\Tmp\libmem> cl demo.c /nologo /MDd /Iinclude /link build-static/libmem.lib
   Creating library demo.lib and object demo.exp
libmem.lib(asm.c.obj) : error LNK2019: unresolved external symbol cs_open referenced in function LM_DisassembleEx
libmem.lib(asm.c.obj) : error LNK2019: unresolved external symbol cs_close referenced in function LM_DisassembleEx
libmem.lib(asm.c.obj) : error LNK2019: unresolved external symbol cs_disasm referenced in function LM_DisassembleEx
libmem.lib(asm.c.obj) : error LNK2019: unresolved external symbol cs_free referenced in function LM_DisassembleEx
libmem.lib(asm.c.obj) : error LNK2019: unresolved external symbol ks_open referenced in function LM_AssembleEx
libmem.lib(asm.c.obj) : error LNK2019: unresolved external symbol ks_close referenced in function LM_AssembleEx
libmem.lib(asm.c.obj) : error LNK2019: unresolved external symbol ks_asm referenced in function LM_AssembleEx
libmem.lib(asm.c.obj) : error LNK2019: unresolved external symbol ks_free referenced in function LM_AssembleEx
demo.exe : fatal error LNK1120: 8 unresolved externals

It compile when I link libmem.lib, capstone.lib, keystone.lib and llvm.lib (LIEF.lid and injector.lib not required here):

PS C:\Users\user\Tmp\libmem> cl demo.c /nologo /MDd /Iinclude /link build-static/libmem.lib build-static/external/capstone-engine-prefix/src/capstone-engine-build/capstone.lib build-static/external/keystone-engine-prefix/src/keystone-engine-build/llvm/lib/keystone.lib build-static/external/llvm/llvm.lib
demo.c
   Creating library demo.lib and object demo.exp
PS C:\Users\user\Tmp\libmem> ./demo.exe
b

Edit: OP also link with all libraries:
image

Edit 2: It works well when also bundling libraries manually on Windows: nathan818fr@d4314b5

@rdbo
Copy link
Owner

rdbo commented Dec 5, 2023

@nathan818fr That's interesting. I really thought MSVC resolved the linking problems for me, apparently not. Good to know.
I'll try your manual bundling for Windows later 👍

@rdbo
Copy link
Owner

rdbo commented Jan 8, 2024

In Python, the setup.py script will fetch a binary release from the GitHub if it has not found a libmem installation in the OS. The same could be done for Rust.

@rdbo
Copy link
Owner

rdbo commented Feb 26, 2024

Now both Python and Rust dynamically fetch the library, avoiding annoyances from building and linking libmem.
The only thing left to do is make this more friendly for C/C++
On Linux, it's pretty good already due to the CMakeLists thing that dynamically fetches and links libmem.
But on Windows, most people will use Visual Studio projects, that's probably what should be aiming for
Either way, a lot of progress on this field 💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants