利用 MinGW-w64 (i686) 移植代码到易语言
本帖最后由 爱飞的猫 于 2023-10-9 05:44 编辑之前试验了下[移植 AES 代码],感觉效果还行,就是还得穿插 `fasm` 来计算偏移什么的有点麻烦。
: https://www.52pojie.cn/thread-1840406-1-1.html
这次试了下行内汇编,可以一次编译得到想要的二进制文件。
这个方案适合的情况:
- 需要简单粗暴的移植几个在易语言里面写起来比较繁琐的函数/算法;
- 降低系统资源 - 在不引用未编译函数的情况下不需要执行任何初始化的代码,数据均随二进制代码和易语言代码一同存储至 `.text` 只读可执行区段内。
这个方案不适合的情况:
- 需要有大量查表数据、调用外部 API(虽然直接爬 PEB 写起来也不麻烦,脚手架写好后就能用了)的情况
- 项目过大。太大的项目编译的字节码也不小,也比较难定位,这样还不如直接编译到 DLL 后引用了。
- 如果只是想文件不落地,可以参考内存加载 DLL 的方案。
## 准备工作
### 1. 安装 MinGW-w64 (i686)
如果已经安装,使用你已经安装好的版本亦可 (如 MSYS2 环境)。
如果还没有,你需要安装一份【预编译的 MinGW-w64】。
本文使用的是 ,如果你需要更新版本可以[查看其发布首页]来获取。[百度网盘存档]
※ Windows 10 开始内置 UCRT 运行时。若系统低于该版本可以[安装 UCRT 运行时],要求 Windows Vista SP2 或更高。
: https://support.microsoft.com/zh-cn/topic/c0514201-7fe6-95a3-b0a5-287930f3560c
: https://pan.baidu.com/s/13RLWIA72AzauK3eyldp_eQ?pwd=w2zp
本文预设你的 mingw32 环境安装在目录 `M:\mingw32`。照着教程操作时请手动替换为你自己的安装路径,该目录下应有一个名为「`i686-w64-mingw32`」的子目录。
推荐带 `llvm` (`clangd`) 的完整版本 `winlibs-i686-mcf-dwarf-gcc-13.2.0-llvm-16.0.6-mingw-w64ucrt-11.0.1-r2.7z`。
如果你不需要 `clangd` 支持也可以使用更小巧的 `winlibs-i686-mcf-dwarf-gcc-13.2.0-mingw-w64ucrt-11.0.1-r2.7z`。
: https://github.com/brechtsanders/winlibs_mingw/releases/tag/13.2.0mcf-16.0.6-11.0.1-ucrt-r2
: https://github.com/brechtsanders/winlibs_mingw/releases/latest
### 2. 准备 VS Code 并配置开发环境 (可选)
本文推荐使用 VS Code + `clangd` LSP 服务。
此处的子步骤可以根据自己需要跳过。
### 中文语言包
官方扩展商店有中文语言包可以安装。
按下 `Ctrl + P`,键入 `ext install MS-CEINTL.vscode-language-pack-zh-hans` 并回车,等待片刻后按照提示重启即可。
### 新建配置文件
强烈推荐建立一个干净的,用于 MinGW-w64 (i686) 的 C++ 环境,避免不同环境的插件干扰。
首先按下左下角的「齿轮」→「配置文件」→「创建配置文件…」。
配置文件名称随意填写,如 `M32 - MinGW-w64 (i686)`,确保复制来源为「无」,然后点击创建。
以后要切换环境时,选择左下角的「齿轮」→「配置文件」→选择该配置环境。
### 安装/配置 clangd
按下 `Ctrl + P`,键入 `ext install llvm-vs-code-extensions.vscode-clangd` 并回车,等待安装完成。
按下「`Ctrl + ,`」打开设置:
- 检索 `clangd.path`,将该值设定为 `M:\mingw32\bin\clangd.exe`。
- 检索 `editor.formatOnSave`,确保该项选中(保存时自动格式化代码)。
- 检索 `files.autoSave`,推荐选择「`onFocusChange`」(切换焦点到终端时自动储存)。
### 测试编译环境
首先准备一个新的目录并使用 VSCode 打开。
点击左下角齿轮,确保「配置文件」菜单显示刚配置的「M32 - MinGW-w64 (i686)」。
左侧的「资源管理器」:
- 建立文件 `src/example_main.cpp`
- 建立文件 `build.cmd`
其中 `build.cmd` 的内容如下:
```batch
::::::::::::::::::::: [初始化环境] 开始
@pushd "%~dp0"
:: 指定 mingw32 目录
@set "MINGW32=M:\mingw32"
@set "PATH=%MINGW32%\i686-w64-mingw32\bin;%MINGW32%\bin;%PATH%"
:: 编译参数
@set "CC=gcc"
@set "CXX=g++"
@set "CXXFLAGS=-O2 -fPIC -Wall"
@set "LDFLAGS="
:: 确保 out 目录存在
@if not exist out @mkdir out
::::::::::::::::::::: [初始化环境] 结束
::::::::::::::::::::: 编译代码放在下面
:::::::::::: [测试文件] 开始
%CXX% %CXXFLAGS% -o out/example.exe src/example_main.cpp
:: 执行编译后的文件:
:: .\out\example.exe
:::::::::::: [测试文件] 结束
```
※ 你也可以自己手写一个 `Makefile` 代替该脚本。本文就不深入了。
其中 `src/example_main.cpp` 的内容如下:
```cpp
#include <cstdio>
int main() {
puts("Hello World!"); // 输出 "Hello World!"
}
```
此时可以按下「`Ctrl + '`」呼出终端,键入 `.\build.cmd` 开始构建。
构建完成后没有消息提示(如果发生错误会输出错误信息),执行 `.\out\example.exe` 可以看到可执行文件能够正常运行:
```batch
M:\demo\e-crc32>.\build.cmd
M:\demo\e-crc32> g++ -O2 -fPIC -Wall -o out/example.exe src/example_main.cpp
M:\demo\e-crc32>.\out\example.exe
Hello World!
```
## 魔改/移植代码
本次代码的移植对象是 `CRC32`。
网络上已经有很多实现了,挑个 [`zlib@v0.9` 里的实现]。
: https://github.com/madler/zlib/blob/v0.9/crc32.c
※ 使用这个古老版本因为没有加入太多「微优化」。
### 理解原始代码
里面的代码看着唬人,其实有用的没多少:
```c
// 预先计算好的常数表
uLong crc_table[] = {
0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L,
// ... 省略 ...
0x2d02ef8dL
};
// 宏 - 处理一次
#define DO1(buf) crc = crc_table[((int)crc ^ (*buf++)) & 0xff] ^ (crc >> 8);
// 函数本体
uLong crc32(crc, buf, len)
uLong crc;
Byte *buf;
uInt len;
{
if (buf == Z_NULL) return 0L;
crc = crc ^ 0xffffffffL;
while (len >= 8)
{
DO8(buf); // 等价于连续写 8 次 "DO1(buf)"。
// 估计是在做什么优化。这一块可以忽略。
len -= 8;
}
if (len) do {
DO1(buf); // 展开这个宏就行。
} while (--len);
return crc ^ 0xffffffffL;
}
```
再将 `crc32` 稍微简化一下:
```c
uint32_t crc32(uint32_t crc, const uint8_t* buf, size_t len) {
if (buf == nullptr) return crc; // 我这里就改成返回传入的 CRC 值了。
crc = ~crc;
while (len--) {
uint8_t idx = crc ^ *buf++;
crc = crc_table ^ (crc >> 8);
}
crc = ~crc;
return crc;
}
```
### 储存查表数据
通常来说,只读的静态数据会被编译器储存到 `.rdata` 区段中。但是我们的代码却在 `.text`。
在 EXE 加载时,系统(EXE 装载代码)会根据 `.reloc` 区段的信息来进行「重定向」修正。
此时有一个问题 - 数据储存在不同的区段,如果需要手动修正这也太麻烦了。如果能直接将数据随代码储存在 `.text` 区段访问就好了…
※ 虽然可以用 `char xxx[] = { ... }` 的写法来内嵌,但是每次执行到该处都会初始化一次…
你可能已经发现了 - x86 并没有「取相对当前可执行地址偏移」的指令(x86-64 倒是有),但是却可以利用 `CALL` 指令来模拟:
```x86asm
call _other_code ; 入栈下一个指令的地址 (_my_data)
; 然后再跳转到目标函数 (_other_code)
_my_data:
db "custom data"
_other_code:
pop eax ; 此时 eax 为 _my_data 的地址,因此出栈就可以得到地址了。
```
在 gcc 编译器套件中,可以利用该特性将资源“内嵌”到代码中:
因此获取表数据就可以利用内联汇编来获取:
```cpp
// 裸函数,不要 inline
__attribute__((naked, noinline)) const uint32_t *get_crc32_table() {
asm("call __exit_crc_table \n"
".incbin \"src/crc_table.bin\" \n" // 引入资源文件
"__exit_crc_table: \n" // "__exit_crc_table" 需要唯一性
"pop %eax \n"
"ret \n");
}
```
※ `src/crc_table.bin` 文件内容为 `crc32_table` 转存至小段序(Little-Endian)后储存的文件。
然后在用到它的函数进行一次初始化即可:
```cpp
#include <cstdint>
#include <cstdio>
// ★★ 将上面的 get_crc32_table 放在此处
int main() {
const auto* crc_table = get_crc32_table();
printf("0x%08x", crc_table); // 0x77073096
}
```
能够正常输出对应的值 `0x77073096`。
### 获取代码数据
大多数情况下,函数出现在内存中的顺序和它们的实现在代码的位置是一致的。
因此我们可以通过在需要包含的所有函数前放置一个 `__crc32_exp_start` 和结尾放置一个 `__crc32_exp_end` 用来辅助定位:
文件 `src/crc32.h`:
```cpp
#pragma once
#include <cstdint>
void __crc32_exp_start();
uint32_t crc32(const uint8_t *buf, uint32_t len, uint32_t crc);
void __crc32_exp_end();
```
文件 `src/crc32.cpp`:
```cpp
#include "crc32.h"
#include <cstdint>
/////////////////////////////////////////////
void __crc32_exp_start() {};
/////////////////////////////////////////////
// ★★ 将上面的 get_crc32_table 放在此处
uint32_t crc32(const uint8_t *buf, uint32_t len, uint32_t crc) {
if (!buf)
return crc;
const auto *crc_table = get_crc32_table();
crc = ~crc;
while (len--) {
uint8_t idx = crc ^ *buf++;
crc = crc_table ^ (crc >> 8);
}
crc = ~crc;
return crc;
}
/////////////////////////////////////////////
void __crc32_exp_end() {}
/////////////////////////////////////////////
```
然后在 `src/crc32_dump.cpp` 加入在运行时提取对应机器码的逻辑:
```cpp
#include "crc32.h"
#include <cassert>
#include <cstdint>
#include <fstream>
int main() {
// 简单测试一下
auto crc_52pojie = crc32((const uint8_t *)"52pojie", 7, 0);
printf("crc32(52pojie) = %08x\n", crc_52pojie);
assert(crc_52pojie == 0x78b5ff78);
auto p_start = (char *)(__crc32_exp_start);
auto p_end = (char *)(__crc32_exp_end);
std::ofstream dump_bin("out/crc32.bin.wav", std::ios::binary);
dump_bin.write(p_start, p_end - p_start);
dump_bin.close();
printf("dump size = %d bytes\n", p_end - p_start);
}
```
随后,在构建脚本底部加入新的编译过程:::::::::::::::::::::: [构建 CRC32 与 dumper] 开始
%CXX% %CXXFLAGS% -c -o out/crc32.cpp.o src/crc32.cpp
%CXX% %CXXFLAGS% -c -o out/crc32_dump.cpp.o src/crc32_dump.cpp
%CXX% %LDFLAGS% -o out/crc32_dump.exe out/crc32.cpp.o out/crc32_dump.cpp.o
.\out\crc32_dump.exe
::::::::::::::::::::: [构建 CRC32 与 dumper] 结束最后执行 `.\build.cmd`,将会编译并抽取这部分的代码到 `out/crc32.bin.wav`。
### 添加引导
因为易语言的函数,大概长这样:' 启动函数调用
函数名称 (1, 2, 3)
' -------------------------------
.子程序 函数名称, 整数型
.参数 参数1, 整数型
.参数 参数2, 整数型
.参数 参数3, 整数型
' 你的代码
返回 (9)易语言生成的机器码 (经过黑月处理) 大概是这样:
```x86asm
push 3
push 2
push 1
call 函数名称 ; 标准 stdcall
; 省略
函数名称:
push ebp
mov ebp, esp ; 即便这个函数没有储存变量,也会设置 ebp
; <---此处开始是我们编写的代码
mov eax, 0x9
jmp @f ; "E9 00 00 00 00" 没啥用的长跳转
@@:
mov esp, ebp
pop ebp
ret 0x0C ; stdcall 调用返回
```
因此,如果我们需要一个能直接在易语言用的「ShellCode」,就得照着易语言的规则来。
回到 `src/crc32.h`,更新一下 `__crc32_exp_start` 的函数签名:
```cpp
__attribute__((naked, noinline)) void __crc32_exp_start();
```
然后到 `src/crc32.cpp` 改写该函数:
```cpp
/////////////////////////////////////////////
__attribute__((naked, noinline)) void __crc32_exp_start() {
// 将易语言的参数重新入栈
asm("mov 0x10(%ebp), %ecx \n");
asm("mov 0x0C(%ebp), %edx \n");
asm("mov 0x08(%ebp), %eax \n");
asm("pushl %ecx");
asm("pushl %edx");
asm("pushl %eax");
// 调用函数
asm("call %0" ::"m"(crc32));
asm("add $0xc, %esp"); // 默认是 cdecl 调用约定,调用方平衡堆栈。
// 还原 ebp 并返回
asm("leave");
asm("ret $0x0C");
};
/////////////////////////////////////////////
```
重新编译,得到一份新的 `crc32.bin.wav` 文件。
### 面向 ShellCode 编程
「置入代码」是易语言提供的一个简单粗暴的内嵌「编译后的机器码」的方案,有点像内联汇编,但是你不能使用名称来引用变量等信息。
首先新建一个易语言程序,依次点击菜单「I.插入」→「R.资源」→「S.声音」,建立一个空白的「声音1」资源。
将名称更为「CRC32_代码」,双击该行右侧的「内容」列,选择「导入新声音(I)」,选择上一步编译得到的「`crc32.bin.wav`」文件。
回到代码区段(程序集1),粘贴下述内容:.版本 2
开始测试()
.子程序 开始测试
.局部变量 crc, 整数型
' 模拟分段计算
crc = CRC32 (到字节集 (“Hello ”))
crc = CRC32 (到字节集 (“World”), crc)
检查 (crc = 1243066710)' 0x4a17b156
' 直接计算
crc = CRC32 (到字节集 (“52pojie”))
检查 (crc = 2025193336)' 0x78b5ff78
.子程序 Ab, 整数型, , 取字节集指针
.参数 变量, 字节集, 参考
置入代码 ({ 139, 69, 8, 139, 0, 131, 192, 8, 139, 229, 93, 194, 4, 0 })
返回 (-1)
.子程序 CRC32, 整数型, 公开, 可重复调用 (如文件分段计算)。后续调用需要提供上一次计算得到的值作为[初始值]。
.参数 内容, 字节集, 参考
.参数 初始值, 整数型, 可空, 若不指定或是初次调用,设为 [#CRC32_默认初始值]。
返回 (CRC32_高级 (Ab (内容), 取字节集长度 (内容), 初始值))
.子程序 CRC32_高级, 整数型, 公开, 可重复调用 (如文件分段计算)。后续调用需要提供上一次计算得到的值作为[初始值]。
.参数 缓冲区地址, 整数型
.参数 缓冲区长度, 整数型
.参数 初始值, 整数型, , 如果是第一次运行,初始值应为 [#CRC32_默认初始值]
置入代码 (#CRC32_代码)
返回 (-1)此时在易语言 IDE 按下 F5 运行,底部的「输出」栏目应当输出下述内容:
```text
* 测试: CRC32
* 完成,没有错误。
```
这表示我们移植的代码可以正常在易语言内运行。
### 优化输出
你可能已经发现之前代码提到的「默认是 `cdecl` 调用约定」。既然是默认的,那自然也可以[使用其他的调用约定]。
: https://gcc.gnu.org/onlinedocs/gcc/x86-Function-Attributes.html
其中一个比较特殊的是 `regparm(3)`,即前三个参数通过 `eax`、`edx` 和 `ecx` 传递,后续的参数再入栈,在三个或更少参数的情况时会会有一定机率让编译的代码变小。
回到 `src/crc32.h` 进行更改: #include <cstdint>
+#ifndef __g_fastcall
+#define __g_fastcall __attribute__((regparm(3)))
+#endif
__attribute__((naked, noinline)) void __crc32_exp_start();
-uint32_t crc32(const uint8_t *buf, uint32_t len, uint32_t crc);
+__g_fastcall uint32_t crc32(const uint8_t *buf, uint32_t len, uint32_t crc);
void __crc32_exp_end();
也对 `src/crc32.cpp` 进行更改: /////////////////////////////////////////////
__attribute__((naked, noinline)) void __crc32_exp_start() {
-// 将易语言的参数重新入栈
+// 将易语言的参数导入过来
+// params: eax, edx, ecx
asm("mov 0x10(%ebp), %ecx \n");
asm("mov 0x0C(%ebp), %edx \n");
asm("mov 0x08(%ebp), %eax \n");
-asm("pushl %ecx");
-asm("pushl %edx");
-asm("pushl %eax");
// 调用函数
asm("call %0" ::"m"(crc32));
-asm("add $0xc, %esp"); // 默认是 cdecl 调用约定,调用方平衡堆栈。
// 还原 ebp 并返回
asm("leave");
asm("ret $0x0C");
};
/////////////////////////////////////////////
@@: 下面一点继续更改~
-uint32_t crc32(const uint8_t *buf, uint32_t len, uint32_t crc) {
+// buf@eax, len@edx, crc@ecx
+__g_fastcall uint32_t crc32(const uint8_t *buf, uint32_t len, uint32_t crc) {
if (!buf)重新编译,然后导入易语言,测试一切正常。
## 其它
胶水代码写起来还是挺麻烦的。
如果要同时导出多个函数,可以考虑将入口的 `__crc32_exp_start` 添加一个参数(如参数序号),然后根据这个值将参数转发到对应的函数。
最终的代码存档:
※ 压缩包中的 `v2023.10.05` 目录为跟着教程走的结果。其他两个目录为后续更新添加的内容,主要为 C++ 方面的性能优化。
---
2023.10.08 更新:
- 加入 CRC32c 实现
- 同时加入了同利用 SSE 4.2 加速指令的优化实现
- 重构代码
- 加入 `crc32_tool.exe` 计算工具和对应的源码
crc32_tool.exe 运算速度特别块,实测[正己老师的第一课](https://www.52pojie.cn/thread-1695141-1-1.html) `第一节.模拟器环境搭建.mp4` 能在 259ms 内计算完成,吞吐率约 23.028 Gbps (大概 2.8GBps)。
```text
> (Measure-Command { .\crc32_tool.exe "M:\Test\test.mp4" | Out-Default }).TotalMilliseconds
M:\Test\test.mp4: 401AF35D
259.5613
```
这应该是全网用易语言实现的 CRC32 算法的最快的一个了
2023.10.07 更新:
- 加入可选的优化
- 查表模式可以选择从 1K 表 → 4K 表 的加速运算;
- 加入 Hagai 的无码表加速模式(默认不使用)
- 加入编译开关接口,附加 `build.default.cmd` 构建推荐构建版本,或 `build.full.cmd` 构建所有支持的模式(查表 4K)
2023.10.06 更新:
找到个基于 SSE 4.2 的 CRC32 计算优化代码:
```cpp
// SSE4.2 + PCLMUL 实现修改自:
// https://chromium.googlesource.com/chromium/src/+/f9a8b512a90f410bb9302a3137855fb688316d3d/third_party/zlib/crc32_simd.c#24
/*
* crc32_sse42_simd_(): compute the crc32 of the buffer, where the buffer
* length must be at least 64, and a multiple of 16. Based on:
*
* "Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction"
*V. Gopal, E. Ozturk, et al., 2009, http://intel.ly/2ySEwL0
*/
```
---
玩具:手动对齐置入代码
本来看编译器这么执着于将代码对齐,于是试了下… 但是发现性能并没有多大变化。
使用 (https://bellard.org/tcc/) 编译,编译出来的是小巧的单文件。
原理很简单,找到程序里的代码区段然后将两个特征码包围的内容前后挪动(两个特征码合起来刚好 16 字节),使其实际代码为对齐 0x10 处开始。
爱飞的猫 发表于 2023-10-8 00:39
加上了,不过实测速度不如利用 `pclmulqdq` 和 SSE 4.2 的优化指令,能在更短的 cycle 时间内处理更多数 ...
这样来看你的那个crc32c_sse42_crc32_proc里面的
while (len >= 4)
{
crc = _mm_crc32_u32(crc, *(const uint32_t *)buf);
len -= 4;
buf += 4;
}
这部分就可以进一步提高吞吐量了,就是32位的可能写起来稍微麻烦了点吧。 DEATHTOUCH 发表于 2023-10-5 13:05
可以考虑顺便再加一个Intel SSE4.2(应该也有15年了)的crc32,虽然多项式不一样,不过用来自校验速度更快 ...
加上了,不过实测速度不如利用 `pclmulqdq` 和 SSE 4.2 的优化指令,能在更短的 cycle 时间内处理更多数据(64 字节一组来计算)。
有个论文但我没读,直接扒的别人代码… CRC32c 部分应该还可以再优化下,如数据不足 64 字节时进行 16 字节块的处理。 好东西,收藏看看 可以考虑顺便再加一个Intel SSE4.2(应该也有15年了)的crc32,虽然多项式不一样,不过用来自校验速度更快,毕竟32位一次4字节。 谢谢分享!收藏学习。 本帖最后由 爱飞的猫 于 2023-10-6 12:28 编辑
DEATHTOUCH 发表于 2023-10-5 13:05
可以考虑顺便再加一个Intel SSE4.2(应该也有15年了)的crc32,虽然多项式不一样,不过用来自校验速度更快 ...
这个过段时间再试,到时候另外整个函数计算。
我倒是找到个利用 SSE 4.2 和 PCLMULQDQ 指令的优化版,计算结果是一致的。 好东西,收藏看看 感谢分享!!! 这个MinGW QT 的程序逆向是什么思路?
页:
[1]
2