减少Rust开发的闭源项目中的调试信息

Rust是一门系统编程语言,专注于安全,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似,但是设计者想要在保证性能的同时提供更好的内存安全。

由于Rust有几千行代码,所以我无论如何都不能自称是Rust专家,但这并不妨碍我对Rust的理解。我非常喜欢Rust的语法,与其他许多语言相比,Rust的具体语法和C,C++类似,都是由花括号限定代码块,还有一样的控制流关键字,例如if,else,while,和for。然而也并非所有的C或者C++关键字被实现了。某些Rust函数(比如关键字match用于模式匹配)对于那些精通这些语言的人就没那么熟悉了。尽管与C/C++极其相似,Rust在深层语法上跟元语言家族的语言像是Haskell更接近。基本上一个函数体的每个部分都是表达式,甚至是控制流操作符。

大约两个月前,我开始开发一个命令行项目,需要一个像样的标准库。最终结果必须是一个本地的、静态链接的可执行文件。实际上,我有C / C ++,Rust或Go几种选择。对于C++来说,STL是不可能的,因为我认为它很糟糕,而且缺少许多必需的功能。Qt是一个漂亮的C++库,我本来想使用它,但是静态链接需要一个商业Qt许可。最重要的是,在多个操作系统上进行编译需要在每个操作系统上重建静态Qt库,因此需要进行大量额外的工作。

但除此之外,还有一个事实是这个项目开发起来很无聊,我想让它变得有趣。所以当我简单地比较了一下Go和Rust时,就不再考虑它了。出于简化的目的,Go缺少了其他语言中存在的许多重要结构。虽然Go的简洁性令人耳目一新并且有其魅力,但我还是觉得复杂的Rust包含许多重要的功能。

需要说明的是,我不喜欢不必要的复杂性,这就是为什么我没有使用C ++中的大多数新功能。我的意思,功能必须要齐全,但是使用过程要简单。 Rust为自己设定了许多目标,其中一些目标很复杂,没有垃圾收集器。Rust不像Go,Java以及.NET Framework那样使用自动垃圾回收系统,不同的是Rust通过RAII来管理内存和资源,还可选引用计数。

说了这么多,Rust在很多方面还不是一种成熟的语言,不能用于任何像C / C ++这样的项目。另外,它的库没有Go的那么丰富,在我看来,它的一些标准库确实非常奇怪,编译也可能非常慢。

尽管Rust是一种可靠且安全的语言,使用起来很有趣。但可以确定地说,我现在还没有看到过Rust的具体用例。相反,我可以看到Go的许多用例,而不是Rust的用例。PHP大马

以下是以我创建的一个样本:

fn foo() {
	panic!("this is an error");
}

fn main() {
    println!("Hello, world!");
	foo();
}

它会打印出来许多元数据:

Hello, world!
thread 'main' panicked at 'this is an error', src\main.rs:2:2
note: Run with 'RUST_BACKTRACE=1' environment variable to display a backtrace.

如果将RUST_BACKTRACE设置为1,情况会更糟:

Hello, world!
thread 'main' panicked at 'this is an error', src\main.rs:2:2
stack backtrace:
   0: std::sys_common::backtrace::print
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858\/src\libstd\sys_common\backtrace.rs:58
   1: std::panicking::default_hook::{{closure}}
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858\/src\libstd\panicking.rs:200
   2: std::panicking::default_hook
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858\/src\libstd\panicking.rs:215
   3: std::panicking::rust_panic_with_hook
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858\/src\libstd\panicking.rs:478
   4: std::panicking::begin_panic
   5: std::panicking::try
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858\/src\libstd\panicking.rs:276
   6: std::panic::catch_unwind
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858\/src\libstd\panic.rs:388
   7: std::rt::lang_start_internal
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858\/src\libstd\rt.r
s:48
   8: main
   9: BaseThreadInitThunk
  10: RtlInitializeExceptionChain
  11: RtlInitializeExceptionChain

我在GitHub的这个线程中发现了这些与闭源项目相关的隐私问题,但是我没有找到任何现成的解决方案。

因此,显而易见的唯一解决方案是修改Rust编译器,这正是我以下要做的。虽然我在Windows上描述了如何执行此操作,但是与构建过程无关的部分在Unix上也是有效的。

为了避免这种麻烦,我简要地研究了Go,以检查在Go二进制文件中可以找到多少元数据。没想到,Go会打印出比Rust更多的元数据。而且它甚至比Rust更糟糕,连修改编译器也会无济于事。

值得一提的另一个原因是,至少在Windows上,Go通过CGo调用c代码的功能时,需要使用mingw编译器,这会导致一系列新的问题。

构建Rust编译器的第一步是下载源代码,你可以从Rust官网或GitHub来下载。然后,你需要Visual Studio 2017或更高版本。文档说的是,Visual Studio版本要在2013年或以上,但我发现有的地方却提到了版本要在2017年或以上。不过,我在使用Visual Studio 2013构建时遇到了困难。所以,我还是建议使用Visual Studio 2017。

我花了一点时间才明白我需要在Visual Studio中安装某些额外的软件包,但如果你在安装过程中想要限制安装大小,以下有一些打钩的软件包,是你绝对需要安装的。

减少Rust开发的闭源项目中的调试信息_第1张图片

你可以在说明书中找到构建说明,以下是具体的说明:

#### MSVC
[windows-msvc]: #windows-msvc

MSVC builds of Rust additionally require an installation of Visual Studio 2013
(or later) so `rustc` can use its linker. Make sure to check the “C++ tools”
option.

With these dependencies installed, you can build the compiler in a `cmd.exe`
shell with:

```sh
> python x.py build
```

Currently, building Rust only works with some known versions of Visual Studio. If
you have a more recent version installed the build system doesn't understand
then you may need to force rustbuild to use an older version. This can be done
by manually calling the appropriate vcvars file before running the bootstrap.

```batch
> CALL "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\amd64\vcvars64.bat"
> python x.py build
```

#### Specifying an ABI
[specifying-an-abi]: #specifying-an-abi

Each specific ABI can also be used from either environment (for example, using
the GNU ABI in PowerShell) by using an explicit build triple. The available
Windows build triples are:
- GNU ABI (using GCC)
    - `i686-pc-windows-gnu`
    - `x86_64-pc-windows-gnu`
- The MSVC ABI
    - `i686-pc-windows-msvc`
    - `x86_64-pc-windows-msvc`

The build triple can be specified by either specifying `--build=` when
invoking `x.py` commands, or by copying the `config.toml` file (as described
in Building From Source), and modifying the `build` option under the `[build]`
section.

我使用的构建三元组是'i686-pc-windows-msvc',因为我需要将应用程序设置为32位,以便最大化兼容性。为此,我将主目录中的'config.toml.example'复制到'config.toml',并修改了其中的一部分内容。

以下是我修改过的部分:

[llvm]

# Indicates whether the LLVM build is a Release or Debug build
optimize = true

# Indicates whether an LLVM Release build should include debug info
release-debuginfo = false

# Indicates whether the LLVM assertions are enabled or not
assertions = false

# Link libstdc++ statically into the librustc_llvm instead of relying on a
# dynamic version to be available.
static-libstdcpp = true

# LLVM targets to build support for.
# Note: this is NOT related to Rust compilation targets. However, as Rust is
# dependent on LLVM for code generation, turning targets off here WILL lead to
# the resulting rustc being unable to compile for the disabled architectures.
# Also worth pointing out is that, in case support for new targets are added to
# LLVM, enabling them here doesn't mean Rust is automatically gaining said
# support. You'll need to write a target specification at least, and most
# likely, teach rustc about the C ABI of the target. Get in touch with the
# Rust team and file an issue if you need assistance in porting!
targets = "X86"

# When invoking `llvm-config` this configures whether the `--shared` argument is
# passed to prefer linking to shared libraries.
link-shared = false

# Build triple for the original snapshot compiler. This must be a compiler that
# nightlies are already produced for. The current platform must be able to run
# binaries of this build triple and the nightly will be used to bootstrap the
# first compiler.
build = "i686-pc-windows-msvc"    # defaults to your host platform

# Flag to specify whether any documentation is built. If false, rustdoc and
# friends will still be compiled but they will not be used to generate any
# documentation.
docs = false

# Indicates whether the native libraries linked into Cargo will be statically
# linked or not.
cargo-native-static = true

[rust]

# Whether or not to optimize the compiler and standard library.
#
# Note: the slowness of the non optimized compiler compiling itself usually
#       outweighs the time gains in not doing optimizations, therefore a
#       full bootstrap takes much more time with `optimize` set to false.
optimize = true

# [...]
debug = false

# Whether or not debug assertions are enabled for the compiler and standard
# library.
debug-assertions = false

# Whether or not `panic!`s generate backtraces (RUST_BACKTRACE)
backtrace = false

[target.i686-pc-windows-msvc]

# Force static or dynamic linkage of the standard library for this target. If
# this target is a host for rustc, this will also affect the linkage of the
# compiler itself. This is useful for building rustc on targets that normally
# only use static libraries. If unset, the target's default linkage is used.
crt-static = true

由于rustc是一个自举编译器,这意味着它使用自己来构建自己。Bootstrapping Compiler,中文叫自举编译器。它的目的是实现自己编译自己。编译器为了达到自己编译自己的目的,它第一个版本必须由其它编程语言来实现,而它的第一个版本通常是非常简单和基础的版本。

很多编程语言发展成熟后都会用该语言本身来编写自己的编译器,比如 C# 和 Go 语言。

构建阶段有3个阶段,称为stage0,stage1和stage2。只有在阶段1中才使用目录中的源代码,然后,生成的编译器在阶段2中再次进行自我构建。具体过程,请点此了解。奇热影视

可以发现,宏非常简单:它位于src / libcore / macros.rs中。

#[macro_export]
#[allow_internal_unstable(core_panic, __rust_unstable_column)]
#[stable(feature = "core", since = "1.6.0")]
macro_rules! panic {
    () => (
        panic!("explicit panic")
    );
    ($msg:expr) => ({
        $crate::panicking::panic(&($msg, file!(), line!(), __rust_unstable_column!()))
    });
    ($msg:expr,) => (
        panic!($msg)
    );
    ($fmt:expr, $($arg:tt)+) => ({
        $crate::panicking::panic_fmt(format_args!($fmt, $($arg)*),
                                     &(file!(), line!(), __rust_unstable_column!()))
    });
}

虽然我的第一个反应就是修补这个宏,但如果我们看一下它里面的内容,就可以看到它调用了macros file!, line! 和__rust_unstable_column!. 。这些宏是在同一个文件中被定义的:

    #[stable(feature = "rust1", since = "1.0.0")]
    #[rustc_doc_only_macro]
    macro_rules! line { () => ({ /* compiler built-in */ }) }

    /// Expands to the column number on which it was invoked.
    ///
    /// For more information, see the documentation for [`std::column!`].
    ///
    /// [`std::column!`]: ../std/macro.column.html
    #[stable(feature = "rust1", since = "1.0.0")]
    #[rustc_doc_only_macro]
    macro_rules! column { () => ({ /* compiler built-in */ }) }

    /// Expands to the file name from which it was invoked.
    ///
    /// For more information, see the documentation for [`std::file!`].
    ///
    /// [`std::file!`]: ../std/macro.file.html
    #[stable(feature = "rust1", since = "1.0.0")]
    #[rustc_doc_only_macro]
    macro_rules! file { () => ({ /* compiler built-in */ }) }

不幸的是,它们是内置的,所以要修改几乎是不可能的。不过从另一方面来说,通过内置,可以防止这些宏在其他地方生成元数据。

所以,我在整个源代码树中搜索了含有“column”的代码行,最终找到了扩展内置宏的位置,即src / libsyntax / ext / source_util.rs。

我修补了其中的内容:

use syntax_pos::{self, Pos, Span, FileName};

// becomes

use syntax_pos::{self, Span, FileName};

// note: this is important because Rust doesn't tolerate unused imports and after these
//       changes the 'Pos' import is no longer used

/// line!(): expands to the current line number
pub fn expand_line(cx: &mut ExtCtxt<'_>, sp: Span, tts: &[tokenstream::TokenTree])
                   -> Box {
    base::check_zero_tts(cx, sp, tts, "line!");

    let topmost = cx.expansion_cause().unwrap_or(sp);
    let loc = cx.source_map().lookup_char_pos(topmost.lo());

    base::MacEager::expr(cx.expr_u32(topmost, loc.line as u32))
}

// becomes

/// line!(): expands to the current line number
pub fn expand_line(cx: &mut ExtCtxt<'_>, sp: Span, tts: &[tokenstream::TokenTree])
                   -> Box {
    base::check_zero_tts(cx, sp, tts, "line!");

    let topmost = cx.expansion_cause().unwrap_or(sp);

    base::MacEager::expr(cx.expr_u32(topmost, 0))
}

/* column!(): expands to the current column number */
pub fn expand_column(cx: &mut ExtCtxt<'_>, sp: Span, tts: &[tokenstream::TokenTree])
                  -> Box {
    base::check_zero_tts(cx, sp, tts, "column!");

    let topmost = cx.expansion_cause().unwrap_or(sp);
    let loc = cx.source_map().lookup_char_pos(topmost.lo());

    base::MacEager::expr(cx.expr_u32(topmost, loc.col.to_usize() as u32 + 1))
}

// becomes

/* column!(): expands to the current column number */
pub fn expand_column(cx: &mut ExtCtxt<'_>, sp: Span, tts: &[tokenstream::TokenTree])
                  -> Box {
    base::check_zero_tts(cx, sp, tts, "column!");

    let topmost = cx.expansion_cause().unwrap_or(sp);

    base::MacEager::expr(cx.expr_u32(topmost, 0))
}

/// file!(): expands to the current filename */
/// The source_file (`loc.file`) contains a bunch more information we could spit
/// out if we wanted.
pub fn expand_file(cx: &mut ExtCtxt<'_>, sp: Span, tts: &[tokenstream::TokenTree])
                   -> Box {
    base::check_zero_tts(cx, sp, tts, "file!");

    let topmost = cx.expansion_cause().unwrap_or(sp);
    let loc = cx.source_map().lookup_char_pos(topmost.lo());
    base::MacEager::expr(cx.expr_str(topmost, Symbol::intern(&loc.file.name.to_string())))
}

// becomes

/// file!(): expands to the current filename */
/// The source_file (`loc.file`) contains a bunch more information we could spit
/// out if we wanted.
pub fn expand_file(cx: &mut ExtCtxt<'_>, sp: Span, tts: &[tokenstream::TokenTree])
                   -> Box {
    base::check_zero_tts(cx, sp, tts, "file!");

    let topmost = cx.expansion_cause().unwrap_or(sp);
	
    base::MacEager::expr(cx.expr_str(topmost, Symbol::intern("")))
}

修改完成后,我们可以打开Visual Studio命令提示符并输入以下命令进行编译:

python.exe x.py build

编译过程需要一段时间。如果你有许多内核,可以尝试通过更改config.toml文件中的相关部分来加快速度。不过构建,也可能以一些奇怪的错误结束。如果你正在编译32位,而LLVM占用了内存,则可能会发生这种情况。不够,这不是一个大问题,只需重新启动构建命令,构建过程将从它停止的地方继续。

如果构建成功结束,你应该在build/i686-pc-windows-msvc/stage e2/bin中使用rustc编译器。由于我没有在该目录中找到任何cargo.exe,所以我只是将其从官方安装中复制了过来。

然后我准备了一个批处理文件来启动Visual Studio命令提示符,以获取正确的Rust版本:

SET PATH=%PATH%;C:\[...]\rustc-1.35.0-src\build\i686-pc-windows-msvc\stage2\bin
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars32.bat"

并通过以下方式编译测试二进制版本:

cargo run --release

输出结果如下所示:

Hello, world!
thread 'main' panicked at 'this is an error', :0:0
error: process didn't exit successfully: `target\release\simple.exe` (exit code: 101)

如果我们设置RUST_BACKTRACE,结果将是相同的。

检查可执行文件后,我们可以看到仍然有一些元数据以绝对路径的形式存在,例如:

Offset     0  1  2  3  4  5  6  7    8  9  A  B  C  D  E  F     Ascii   

0001BE40  4C 6F 63 6B 53 68 61 72   65 64 00 00 30 10 40 00     LockShared..0.@.
0001BE50  04 00 00 00 04 00 00 00   C0 93 40 00 70 92 40 00     [email protected].@.
0001BE60  70 93 40 00 00 00 00 00   00 00 00 00 00 00 00 00     p.@.............
0001BE70  43 3A 5C 55 73 65 72 73   5C 63 5C 72 75 73 74 5F     C:\Users\c\rust_
0001BE80  62 75 69 6C 64 73 5C 72   75 73 74 63 2D 31 2E 33     builds\rustc-1.3
0001BE90  35 2E 30 2D 73 72 63 5C   73 72 63 5C 6C 69 62 63     5.0-src\src\libc
0001BEA0  6F 72 65 5C 66 6D 74 5C   6D 6F 64 2E 72 73 00 00     ore\fmt\mod.rs..
0001BEB0  70 CE 41 00 3E 00 00 00   63 01 00 00 13 00 00 00     p.A.>...c.......
0001BEC0  C0 CE 41 00 00 00 00 00   00 00 00 00 00 00 00 00     ..A.............
0001BED0  3A 20 00 00 C0 CE 41 00   00 00 00 00 D0 CE 41 00     :.....A.......A.

我能找到的所有路径都与编译器的路径有关,而与项目的路径无关,如果你对它们感到困扰,那么很容易编写一个简单的Python脚本将它们归零作为构建后的步骤。

除了libc没有静态链接到我们的可执行文件中,现在我们可以开始了。如果我们看一下导入表,我们可以看到较新版本的Visual Studio产生的导入结果。

减少Rust开发的闭源项目中的调试信息_第2张图片

要解决这个问题,我们需要像以下这样调用rustc:

rustc -C target-feature=+crt-static ...

我是在这里找到的相关的文档。我们可以通过设置环境变量RUSTFLAGS来对cargo进行标识:

RUSTFLAGS='-C target-feature=+crt-static'

为此,我像这样修改了我的批处理脚本:

SET RUSTFLAGS=-C target-feature=+crt-static
SET PATH=%PATH%;C:\[...]\rustc-1.35.0-src\build\i686-pc-windows-msvc\stage2\bin
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars32.bat"

这样,我将得到一个更大的可执行文件,除了kernel32之外没有外部依赖,完美!

此时,我们只需要从PE中删除调试目录,你可以使用Cerbero Suite或CFF Explorer的简单脚本来完成此操作。说实话,在我的程序中,我仍然使用CFF资源管理器脚本,而且从来没有为Cerbero工具编写过这样的脚本。

function fixdbg(name)
    local h = OpenFile(dir .. "\\" .. name)
    if h == null then
        MsgBox("fail")
        return
    end
    RemoveDebugDirectory(h)
    UpdateChecksum(h)
    SaveFile(h)
    -- don't close, otherwise it fails, don't know why
end

dir = GetCurrentDirectory()

-- list of files to fix
fixdbg("app.exe")

你可以调用此脚本fixdbg.cff并直接启动它,因为cff扩展名与CFF Explorer相关联,所以,我可以将其安排为构建后的步骤。

让我们通过最大化兼容性来很好地完成这个任务,至此,我们就有了一个干净的、静态链接的可执行文件。你可以尝试让它在XP上运行,不过需要修改Portable Executable的Optional Header中的一些字段。

减少Rust开发的闭源项目中的调试信息_第3张图片

修改的字段如下所示:

MajorOperatingSystemVersion: 5

MinorOperatingSystemVersion: 0

MajorSubsystemVersion: 5

MinorSubsystemVersion: 0

尝试在XP上运行:

减少Rust开发的闭源项目中的调试信息_第4张图片

可以看到,我们得到了一个经过处理的Rust可执行文件。

在强调一遍,要使用最新的Rust编译器和在Windows XP上运行的Visual Studio 2017来进行构建。

你可能感兴趣的:(减少Rust开发的闭源项目中的调试信息)