背景信息

2024-12-21 11:58:11 (UTC)

  1. 准备交易:
    https://app.blocksec.com/explorer/tx/eth/0x72a252277e30ea6a37d2dc9905c280f3bc389b87f72b81a59aa8f50baebd8eaa

2025-01-04 11:59:23 (UTC)

  1. 攻击交易 1:
    https://app.blocksec.com/explorer/tx/eth/0x6439d63cc57fb68a32ea8ffd8f02496e8abad67292be94904c0b47a4d14ce90d
  2. 攻击交易 2:
    https://app.blocksec.com/explorer/tx/eth/0xf1a494239af59cd4c1d649a1510f0beab8bb78c62f31e390ba161eb2c29fbf8b
  3. 攻击交易 3:
    https://app.blocksec.com/explorer/tx/eth/0x09b26b87a91c7aea3db05cfcf3718c827eba58c0da1f2bf481505e0c8dc0766b

漏洞合约:
https://vscode.blockscan.com/ethereum/0x5d16b8ba2a9a4eca6126635a6ffbf05b52727d50

sorraStaking
项目是一个质押奖励项目,用户质押 SOR 代币并锁定一段时间,解锁后获取 SOR 代币作为奖励。

Trace 分析

准备交易

在 2024-12-21 11:58:11 (UTC),攻击者进行了一笔
deposit
操作。

image

攻击交易

由于三笔攻击交易类似,所以只分析其中一笔。


deposit
操作的 13 天后,攻击者进行了后续的攻击操作。攻击者反复调用
withdraw
函数,虽然每次只取回
_amount = 1
的代币,但是能够获得大量的奖励代币。

image

然后将获得的 SOR 代币进行出售,最后支付 bundler 的费用。

image

代码分析

当用户执行
deposit
进行质押时,合约收取质押代币,并记录仓位

image

deposite → _updatePosition → _increasePosition

_increasePosition
函数记录质押的数量,时间和利率等相关信息。

image


withdraw
函数中,会先计算用户的奖励代币数量
rewardAmount
,然后将
(_amount + rewardAmount)
一同发给用户。

image

通过
_calculateRewards
函数来计算奖励金额。

image

代码问题:

  1. 每次所领取的奖励金额都是按照用户质押的总金额来计算的,而不是当前取款数额对应的奖励金额。
  2. withdraw
    函数可以将质押金额进行分批提取。

所以攻击者通过多次调用
withdraw
函数来赎回部分质押代币,却每次都能获取到对应所有质押金额的奖励代币,最终获得超额的收益。

一、概述

Spring AI

Spring
官方社区项目,旨在简化
Java AI
应用程序开发,让
Java
开发者像使用
Spring
开发普通应用一样开发
AI
应用。

Spring Cloud Alibaba AI
是一个将
Spring Cloud
微服务生态与阿里巴巴
AI
能力无缝集成的框架,帮助开发者快速构建具备
AI
功能的现代化应用。本文将介绍
Spring Cloud Alibaba AI
的基本概念、主要特性和功能,并演示如何完成一个
在线聊天

在线画图

AI
应用。

二、主要特性和功能

Spring Cloud Alibaba AI
目前基于
Spring AI 0.8.1
版本 API 完成通义系列大模型的接入。通义接入是基于阿里云
阿里云百炼
服务;而
阿里云百炼
建立在
模型即服务(MaaS)
的理念基础之上,围绕
AI
各领域模型,通过标准化的
API
提供包括模型推理、模型微调训练在内的多种模型服务。

主要提供以下核心功能:

2.1. 简单易用的集成

通过
Spring Boot
风格的自动配置机制,开发者只需少量代码配置,即可快速接入阿里云的
AI
服务。

2.2. 丰富的 AI 服务支持

支持以下核心能力:

  • 自然语言处理(NLP)
    :文本分析、智能问答、翻译。
  • 计算机视觉(CV)
    :图像生成、图像识别、目标检测。
  • 语音处理
    :语音识别、语音合成。
  • 数据分析与预测
    :数据建模、趋势分析。

2.3. 高度扩展性

通过配置中心和注册中心(如 Nacos)实现动态扩展,支持微服务架构的扩展需求。
提供接口定义,方便接入第三方
AI
平台。

三、构建 AI 应用

Spring Cloud Alibaba AI 对 Java 版本有要求,所以需要提前预装好 Java 17 环境。

3.1. 申请 API-KEY

登录阿里云,进入
阿里云百炼
的页面:

https://bailian.console.aliyun.com/?apiKey=1#/api-key

创建自己的
API-KEY

3.2. 添加依赖


Spring Boot
项目的
pom.xml
中添加
alibaba-ai
依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-ai</artifactId>
</dependency>

<repositories>
    <repository>
        <id>alimaven</id>
        <url>https://maven.aliyun.com/repository/public</url>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-snapshots</id>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

3.3. 配置 API-KEY


application.yml
中配置 Kafka 的相关属性,包括服务器地址、认证信息等。

spring:
  cloud:
    ai:
      tongyi:
        connection:
          api-key: sk-xxxxxx
  • api-key
    配置在阿里云百炼里申请的api-key

3.4. 创建模型调用服务

@Service
@Slf4j
public class TongYiSimpleService {
    @Resource
    private TongYiChatModel chatClient;
    @Resource
    private TongYiImagesModel imageClient;

    public String chat(String message) {
        Prompt prompt = new Prompt(new UserMessage(message));
        return chatClient.call(prompt).getResult().getOutput().getContent();
    }

    public String image(String message) {
        ImagePrompt prompt = new ImagePrompt(message);
        Image image = imageClient.call(prompt).getResult().getOutput();
        return image.getB64Json();
    }
}

聊天和图片的服务,分别通过注入
TongYiChatModel

TongYiImagesModel
对象来实现,屏蔽底层通义大模型交互细节。

3.5. 创建controller

@RestController
@RequestMapping("/ai")
public class TongYiController {
    @Resource
    private TongYiSimpleService tongYiSimpleService;

    @GetMapping("/chat")
    public String chat(@RequestParam(value = "message") String message) {
        return tongYiSimpleService.chat(message);
    }

    @GetMapping("/image")
    public ResponseEntity<byte[]> image(@RequestParam(value = "message") String message) {
        String b64Str = tongYiSimpleService.image(message);
        byte[] imageBytes = Base64.getDecoder().decode(b64Str);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.IMAGE_JPEG);
        return new ResponseEntity<>(imageBytes, headers, HttpStatus.OK);
    }
}

3.6. 测试效果

3.6.1. 聊天接口

在浏览器输入:
http://localhost:8009/ai/chat?message=你是谁

3.6.2. 图片接口

在浏览器输入:
http://localhost:8009/ai/image?message=意大利面拌42号混凝土

3.6.3. 搭配聊天页面

四、总结

当前版本的
Spring Cloud Alibaba AI
主要完成了几种常见生成式模型的适配,涵盖对话、文生图、文生语音等。在未来的版本中将继续推进
VectorStore

Embedding

ETL Pipeline

RAG
等更多
AI
应用开发场景的建设。

完整的样例代码下载:
https://gitee.com/zlt2000/spring-cloud-ai-sample

AES对称加密

AES(Advanced Encryption Standard),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。

高级加密标准(AES,Advanced Encryption Standard)是密码学中的高级加密标准,AES为分组加密法,把明文分成一组一组的,每组长度相等,每次加密一组数据,直到加密完整个明文,在AES标准规范中,分组长度只能是128位,AES是按照字节进行加密的,也就是说每个分组为16个字节(每个字节8位)。密钥的长度可以使用128位、192位或256位。这导致密钥长度不同,推荐加密的轮数也不同。

  • 明文P

没有经过加密的数据

  • 密钥K

用来加密明文的密码,在对称加密算法中,加密与解密的密钥是相同的。密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,通常是通过非对称加密算法加密密钥,然后再通过网络传输给对方,或者直接面对面商量密钥。密钥是绝对不可以泄漏的,否则会被攻击者还原密文,窃取机密数据

  • AES加密函数

设AES加密函数为E,则 C = E(K, P),其中P为明文,K为密钥,C为密文。也就是说,把明文P和密钥K作为加密函数的参数输入,则加密函数E会输出密文C

  • 密文C

经加密函数处理后的数据

  • AES解密函数

设AES解密函数为D,则 P = D(K, C),其中C为密文,K为密钥,P为明文。也就是说,把密文C和密钥K作为解密函数的参数输入,则解密函数会输出明文P

Python Crypto 加密库

# python不同版本不一样
pip install pycryptodome   (python3.7.3)

pip install cryptography    (python3.12.2)

python3.7.3 版本

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad


def encrypt_config(config_file, encrypted_file):
    """加密配置文件"""

    # 生成一个 16 字节的随机密钥
    key = get_random_bytes(16)
    print("Key: ", key)

    # 读取配置文件
    with open(config_file, 'rb') as f:
        plaintext = f.read()

    # 加密
    cipher = AES.new(key, AES.MODE_CBC)
    ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))

    with open(encrypted_file, 'wb') as f:
        f.write(cipher.iv)
        f.write(ciphertext)
    print(f'File encrypted: {encrypted_file}')


def encrypt_config_one_key(config_files, encrypted_files):
    """加密配置文件,多个文件用同一个密钥"""

    if not isinstance(config_files, list) or not isinstance(encrypted_files, list):
        raise TypeError('config_files and encrypted_files must be list')

    # 生成一个 16 字节的随机密钥
    key = get_random_bytes(16)
    print("Key: ", key)

    config_add_key(config_files[0], encrypted_files[0], key)
    config_add_key(config_files[1], encrypted_files[1], key)
    return


def config_add_key(config_file, encrypted_file, key):

    # 读取配置文件
    with open(config_file, 'rb') as f:
        plaintext = f.read()

    # 加密
    cipher = AES.new(key, AES.MODE_CBC)
    ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))

    with open(encrypted_file, 'wb') as f:
        f.write(cipher.iv)
        f.write(ciphertext)
    print(f'File encrypted: {encrypted_file}')


def decrypt_config(encrypted_file, key):
    """解密配置文件"""
    # 解密
    with open(encrypted_file, 'rb') as f:
        iv = f.read(16)
        encrypted_data = f.read()

    cipher = AES.new(key, AES.MODE_CBC, iv)
    config_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)

    print(f'解密后的数据: {config_data.decode()}')



if __name__ == '__main__':
    # file1 = "/home/pi/printer_data/config/printer.cfg"
    # file2 = "/home/pi/printer_data/config/flsun_func.cfg"
    # target1 = "/home/pi/printer_data/config/test/printer_test.cfg"
    # target2 = "/home/pi/printer_data/config/test/flsun_func.cfg"
    #
    # # encrypt_config(file1, target1)
    # # encrypt_config(file2, target2)
    # key1 = b'.)7[\x8c\xab\x12Y\xd6tm\x06\xe2\xd1l\xb5'
    # key2 = b'\xf9\x19Uf\x11\xe9q\x0ci\x87\xe32%\xf8:W'
    # # decrypt_config(target1, key1)
    # decrypt_config(target2, key2)
    config_files = ["/home/pi/printer_data/config/printer.cfg", "/home/pi/printer_data/config/flsun_func.cfg"]
    target_files = ["/home/pi/printer_data/config/test/printer_test.cfg", "/home/pi/printer_data/config/test/flsun_func.cfg"]

    # encrypt_config_one_key(config_files, target_files)

    key = b'F.r\xeaH\xfb\xf5\x9d\xe9_\x1b\xba\rW`\xaa'

    # decrypt_config(target_files[0], key)
    decrypt_config(target_files[1], key)

python3.12.2 版本


import pickle
import configparser

from cryptography.fernet import Fernet
import os

def cfg2bin(file, result_file):
    # 读取配置文件
    config = configparser.ConfigParser()
    config.read(file)
    # 将配置文件对象序列化为二进制文件
    with open(result_file, 'wb') as f:
        pickle.dump(config, f)


def encrypt_config(config_file, encrypted_file):
    """加密配置文件"""
    # 生成密钥
    key = Fernet.generate_key()
    print("Key: ", key)

    # 读取配置文件内容
    with open(config_file, 'r') as f:
        config_data = f.read()

    # 使用密钥加密配置数据
    fernet = Fernet(key)
    encrypted_data = fernet.encrypt(config_data.encode())

    # 写入加密后的文件
    with open(encrypted_file, 'wb') as f:
        f.write(encrypted_data)

    print(f'配置文件已加密并保存为: {encrypted_file}')


def decrypt_config(encrypted_file, key):
    """解密配置文件"""
    # 读取加密后的文件
    with open(encrypted_file, 'rb') as f:
        encrypted_data = f.read()

    # 使用密钥解密配置数据
    fernet = Fernet(key)
    config_data = fernet.decrypt(encrypted_data).decode()

    print(f'解密后的数据: {config_data}')


if __name__ == '__main__':
    file1 = "/home/cln/printer_data/config/printer.cfg"
    file2 = "/home/cln/printer_data/config/mainsail.cfg"
    target1 = "/home/cln/printer_data/config/test/printer_test.cfg"
    target2 = "/home/cln/printer_data/config/test/mainsail.cfg"

    # encrypt_config(file1, target1)
    # encrypt_config(file2, target2)
    key1 = b'FN8eGSYuXtK2K7-X2jUcm6r1FK16PAzdTMOpg6wyLGQ='
    key2 = b'dbBmLzpyO20jqphgQsIbCPxIpNAG-q7s6E8rxu3K4Wo='
    decrypt_config(target1, key1)
    # decrypt_config(target2, key2)

加密效果

涉及模块

  • moonraker 动态修改配置文件模块
  • klippy 读取配置模块

挤出机配置剥离

  • 在打印机初始化加载完配置文件后,只针对挤出机的参数进行赋值,达到弃用配置文件里的参数值
  • 挤出机配置参数赋值实现,可以创建一个常量类(放在util目录下),或者单独创建一个挤出机的ini配置,放在klippy目录下
  • 或者在toolhead模块加载挤出机对象之前进行挤出机配置参数赋值

  • 配置文件参数

  • extruder 用到的所有参数值信息

前言

一个WIFI上位机,接收底层MPU6050数据,途中转蓝牙从机透传,到蓝牙主机直连WIFI,PC端UDP通信,实现三轴加速度数据传送和计步功能。

项目介绍

本项目基于.NET平台,使用WPF开发了一个应用程序,用于实现MPU6050传感器数据从蓝牙模块传输到主机,并通过WiFi以UDP协议接收这些数据并进行可视化展示。

具体而言,MPU6050作为从设备通过蓝牙连接到主控设备(蓝牙主机),再由主控设备经由WiFi网络将数据以UDP包的形式发送至服务器或客户端进行处理和图形化显示。

项目运行环境

1、开发平台

项目基于.NET Framework,采用 WPF 开发用户界面。

2、集成开发环境

Visual Studio 2019,确保已安装 C# 开发工作负载以支持项目开发与运行。

3、启动项目

获取源代码后,通过打开 BluetoothPC.sln 解决方案文件即可。确保所有依赖项正确配置后,直接运行解决方案,应用程序应能正常启动。

4、用户界面特色

应用程序配备了一个直观且美观的用户界面,图标设计精心,提供优秀的用户体验和视觉享受。

项目设计流程

1、设计框架

前台XAML的UI设计不过多介绍,主要看设计需求,逃不开模板、触发器、动画、样式之类的技术。

后台C#在UI主线程下开了三个子线程:

1、UDP数据监听接收线程。

2、三轴数据UI更新线程。

3、计步和进度条数据更新线程。

因没有碰到多个线程访问同一个UI控件或写同一个UI控件,所以没用到锁,但内部加了异步延迟,让UI更新顺滑一些。

2、服务器连接设计

UDP端IP地址和端口号需正常且有效,加了错误弹窗,若出现下图请重新输入:

3、三轴加速度显示

有硬件:成功连接上之后需配合底层硬件,这里是接收UDP发过来的3轴加速度值。

无硬件:如果没有硬件也行,自行找个网络调试助手,开个UDP服务,本机连接就行,发送的数据需包含以下格式:

任意字符(:1.23938 mG)任意字符。

解释:

发送过来的数据必须包含在 :xxxx mG 内,冒号和mG不能少,可任意多组,每组代表一轴数据。

4、计步显示

利用三轴加速度提供的数据处理步数。需打开左下角计步控制按钮。

打开后弹出提示:

本计步算法仅支持手臂摆动的峰峰值计步,若有更好的算法请分享,万分感谢!!!

计步程序如下

/** valueNum - 存放三轴数据(x,y,z)的个数
* tempValue - 用于存放计算阈值的波峰波谷差值的数组(在这个方法里存放值数组长度为5)
* isDirectionUp - 是否上升的标志位
* continueUpCount - 持续上升的次数
* continueUpFormerCount - 上一点的持续上升的次数,为了记录波峰的上升次数
* lastStatus - 上一点的状态,上升还是下降
* peakOfWave - 波峰值
* valleyOfWave - 波谷值
* timeOfThisPeak - 此次波峰的时间
* timeOfLastPeak - 上次波峰的时间
* timeOfNow - 当前的时间
* gravityOld - 上次传感器的值
* initialValue - 动态阈值需要动态的数据,这个值用于这些动态数据的阈值,这个值是由大量数据得来的
* ThreadValue - 初始阈值,这个值是由大量数据得来的
* minValue - 初始最小值 计算出来的xyz数值乘重力加速度(9.8),此为手机拿在手里(不摆臂)(由自己多次测试得出的值)
* maxValue - 初始最大值 自己设定的最大值(我们定位2)乘重力加速度(9.8),此为手机拿在手里(不摆臂)(由自己多次测试得出的值)
* g - 重力加速度(9.8)
* thisSteps 步数
*/ private int valueNum = 5;//private double[] tempValue; private List<double> tempValue = new List<double>();private Boolean isDirectionUp = false;private int continueUpCount = 0;private int continueUpFormerCount = 0;private Boolean lastStatus = false;private double peakOfWave = 0;private double valleyOfWave = 0;private double timeOfThisPeak = 0;private double timeOfLastPeak = 0;private double timeOfNow = 0;private double gravityOld = 0;private double initialValue = 1.7;private double ThreadValue = 2.0;private double minValue = 11;private double maxValue = 19.6;private double g = 9.8;private double thisSteps = 0;//当前步数 private double StepsCopy = 0;//步数复制 /// <summary> ///监测新的步数 如果检测到了波峰,并且符合时间差以及阈值的条件,则判定为1步///符合时间差条件,波峰波谷差值大于initialValue,则将该差值纳入阈值的计算中/// </summary> /// <param name="_values">加速传感器三轴的平均值</param> public void detectorNewStep(double_values)
{
if (gravityOld == 0)
{
gravityOld
=_values;
}
else{if(detectorPeak(_values, gravityOld))
{
timeOfLastPeak
=timeOfThisPeak;
timeOfNow
= Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds);//时间差大于200ms,小于2s if (((timeOfNow - timeOfLastPeak) >= 200) && ((timeOfNow - timeOfLastPeak) <= 2000) && ((peakOfWave - valleyOfWave) >=ThreadValue))
{
timeOfThisPeak
=timeOfNow;//增加步数 thisSteps++;//增加步数复制 StepsCopy++;
}
if(((timeOfNow - timeOfLastPeak) >= 200) && ((peakOfWave - valleyOfWave) >=initialValue))
{
timeOfThisPeak
=timeOfNow;double _diffWaveVal = peakOfWave -valleyOfWave;
ThreadValue
=peak_Valley_Thread(_diffWaveVal);
}
}
gravityOld
=_values;
}
}
/// <summary> ///监测波峰///以下四个条件判断为波峰///1.目前点为下降的趋势:isDirectionUp为false///2.之前的点为上升的趋势:lastStatus为true///3.到波峰为止,持续上升大于等于2次///4.波峰值大于minValue,小于maxValue///记录波谷值///1.观察波形图,可以发现在出现步子的地方,波谷的下一个就是波峰,有比较明显的特征以及差值///2.所以要记录每次的波谷值,为了和下次的波峰作对比/// </summary> /// <param name="_newValue"></param> /// <param name="_oldValue"></param> /// <returns></returns> public Boolean detectorPeak(double _newValue, double_oldValue)
{
lastStatus
=isDirectionUp;if (_newValue >=_oldValue)
{
isDirectionUp
= true;
continueUpCount
++;
}
else{
continueUpFormerCount
=continueUpCount;
continueUpCount
= 0;
isDirectionUp
= false;
}
if (!isDirectionUp && lastStatus && (continueUpFormerCount >= 2 && (_oldValue >= minValue && _oldValue <maxValue)))
{
//满足上面波峰的四个条件,此时为波峰状态 peakOfWave =_oldValue;return true;
}
else if (!lastStatus &&isDirectionUp)
{
//满足波谷条件,此时为波谷状态 valleyOfWave =_oldValue;return false;
}
else{return false;
}
}
/// <summary> ///阈值的计算///1.通过波峰波谷的差值计算阈值///2.记录4个值,存入tempValue[] 数组中///3.在将数组传入函数averageValue中计算阈值/// </summary> /// <param name="_value"></param> /// <returns></returns> public double peak_Valley_Thread(double_value)
{
double _tempThread =ThreadValue;
List
<double> _tempValue = new List<double>(tempValue);if (tempValue.Count <valueNum)
{
tempValue.Add(_value);
}
else{//tempValue数组长度=valueNum=5 _tempThread =averageValue(tempValue);

_tempValue.RemoveAt(
0);
_tempValue.Add(_value);
tempValue
=_tempValue;
}
return_tempThread;
}
/// <summary> ///梯度化阈值///1.计算数组的均值///2.通过均值将阈值梯度化在一个范围里///这些数据是通过大量的统计得到的/// </summary> /// <param name="_value"></param> /// <returns></returns> public double averageValue(List<double>_value)
{
if (_value.Count != 0)
{
double _ave = 0;foreach (double i in_value)
_ave
+=i;
_ave
= _ave /_value.Count;if(_ave >= 8)
{
_ave
= 4.3;
}
else if (_ave >= 7 && _ave < 8)
{
_ave
= 3.3;
}
else if (_ave >= 4 && _ave < 7)
{
_ave
= 2.3;
}
else if (_ave >= 3 && _ave < 4)
{
_ave
= 2.0;
}
else{
_ave
= 1.7;
}
return_ave;
}
else{return 1.7;
}
}

计步效果如下所示:

开启步数控制按钮后总步数累加,进度条进度为50步,每到达50步距离弹出提示框,计步结束也弹提示框通知。

5、倾力UI按钮设计

设计了一组拟物化按钮,目前无任何功能,有需要的小伙伴自行更改设计功能。

项目地址

Gitee:
https://gitee.com/tytokongjian/StepCountingUpperPC

总结

以上仅展示了蓝牙转WIFI计步上位机的部分功能。更多实用特性和详细信息,请大家访问项目地址。

希望通过本文能为上位机机开发方面提供有价值的参考。欢迎在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

一:背景

1. 讲故事

前段时间训练营里的一位朋友提了一个问题,我用ReadAsync做文件异步读取时,我知道在Win32层面会传 lpOverlapped 到内核层,那在内核层回头时,它是如何通过这个 lpOverlapped 寻找到 ReadAsync 这个异步的Task的呢?

这是一个好问题,这需要回答人对异步完整的运转流程有一个清晰的认识,即使有清晰的认识也不能很好的口头表述出来,就算表述出来对方也不一定能听懂,所以干脆开两篇文章来尝试解读一下吧。

二:lpOverlapped 如何映射

1. 测试案例

为了能够讲清楚,我们先用
fileStream.ReadAsync
方法来写一段异步读取来产生Overlapped,参考代码如下:


        static void Main(string[] args)
        {
            UseAwaitAsync();
            Console.ReadLine();
        }

        static async Task<string> UseAwaitAsync()
        {
            string filePath = "D:\\dumps\\trace-1\\GenHome.DMP";
            Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} 请求发起...");
            FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 16, useAsync: true);
            {
                byte[] buffer = new byte[fileStream.Length];

                int bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length);

                string content = Encoding.UTF8.GetString(buffer, 0, bytesRead);

                var query = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} 获取到结果:{content.Length}";

                Console.WriteLine(query);

                return query;
            }
        }

很显然上面的方法会调用 Win32 中的 ReadFile,接下来上一下它的签名和 _OVERLAPPED 结构体。


BOOL ReadFile(
  [in]                HANDLE       hFile,
  [out]               LPVOID       lpBuffer,
  [in]                DWORD        nNumberOfBytesToRead,
  [out, optional]     LPDWORD      lpNumberOfBytesRead,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

typedef struct _OVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    } DUMMYSTRUCTNAME;
    PVOID Pointer;
  } DUMMYUNIONNAME;
  HANDLE    hEvent;
} OVERLAPPED, *LPOVERLAPPED;

2. 寻找映射的两端

既然是映射嘛,肯定要找到两个端口,即非托管层的 NativeOverlapped 和 托管层的 ThreadPoolBoundHandleOverlapped。

  1. 非托管 _OVERLAPPED

在 C# 中用 NativeOverlapped 结构体表示 Win32 的 _OVERLAPPED 结构,参考如下:


public struct NativeOverlapped
{
	public nint InternalLow;
	public nint InternalHigh;
	public int OffsetLow;
	public int OffsetHigh;
	public nint EventHandle;
}

  1. 托管 ThreadPoolBoundHandleOverlapped

ReadAsync 所产生的
Task<int>
在底层是经过ValueTask, OverlappedValueTaskSource 一阵痉挛后弄出来的,最后会藏匿在 Overlapped 子类的 ThreadPoolBoundHandleOverlapped 中,参考代码和模型图如下:


        public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        {
            ValueTask<int> valueTask = this.ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken);
            if (!valueTask.IsCompletedSuccessfully)
            {
                return valueTask.AsTask();
            }
            return this._lastSyncCompletedReadTask.GetTask(valueTask.Result);
        }

        private unsafe static ValueTuple<SafeFileHandle.OverlappedValueTaskSource, int> QueueAsyncReadFile(SafeFileHandle handle, Memory<byte> buffer, long fileOffset, CancellationToken cancellationToken, OSFileStreamStrategy strategy)
        {
            SafeFileHandle.OverlappedValueTaskSource overlappedValueTaskSource = handle.GetOverlappedValueTaskSource();
            
            NativeOverlapped* ptr = overlappedValueTaskSource.PrepareForOperation(buffer, fileOffset, strategy);
            if (Interop.Kernel32.ReadFile(handle, (byte*)overlappedValueTaskSource._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, ptr) == 0)
            {
                overlappedValueTaskSource.RegisterForCancellation(cancellationToken);
            }
            overlappedValueTaskSource.FinishedScheduling();
            return new ValueTuple<SafeFileHandle.OverlappedValueTaskSource, int>(overlappedValueTaskSource, -1);
        }

最后就是两端的映射关系了,先通过 malloc 分配了一块私有内存,中间隔了一个refcount 的 8byte大小,模型图如下:

3. 眼见为实

要想眼见为实,可以从C#源码中的
Overlapped.AllocateNativeOverlapped
方法寻找答案。


    public unsafe class Overlapped
    {
        private NativeOverlapped* AllocateNativeOverlapped(object? userData)
        {
            NativeOverlapped* pNativeOverlapped = null;

            nuint handleCount = 1;

            pNativeOverlapped = (NativeOverlapped*)NativeMemory.Alloc((nuint)(sizeof(NativeOverlapped) + sizeof(nuint)) + handleCount * (nuint)sizeof(GCHandle));

            GCHandleCountRef(pNativeOverlapped) = 0;

            pNativeOverlapped->InternalLow = default;
            pNativeOverlapped->InternalHigh = default;
            pNativeOverlapped->OffsetLow = _offsetLow;
            pNativeOverlapped->OffsetHigh = _offsetHigh;
            pNativeOverlapped->EventHandle = _eventHandle;

            GCHandleRef(pNativeOverlapped, 0) = GCHandle.Alloc(this);
            GCHandleCountRef(pNativeOverlapped)++;

            return pRet;
        }

        private static ref nuint GCHandleCountRef(NativeOverlapped* pNativeOverlapped)
                               => ref *(nuint*)(pNativeOverlapped + 1);

        private static ref GCHandle GCHandleRef(NativeOverlapped* pNativeOverlapped, nuint index)
                              => ref *((GCHandle*)((nuint*)(pNativeOverlapped + 1) + 1) + index);
    }

卦中代码先用
NativeMemory.Alloc
方法分配了一块私有内存,随后还把 Overlapped 给 GCHandle.Alloc 住了,这是防止异步期间对象被移动,有了代码接下来上windbg去眼见为实,在
Kernel32!ReadFile
中下断点观察方法的第五个参数。


0:000> bp Kernel32!ReadFile
0:000> g
Breakpoint 0 hit
KERNEL32!ReadFile:
00007ffd`fa2f56a0 ff25caca0500    jmp     qword ptr [KERNEL32!_imp_ReadFile (00007ffd`fa352170)] ds:00007ffd`fa352170={KERNELBASE!ReadFile (00007ffd`f85c5520)}
0:000> k 5
 # Child-SP          RetAddr               Call Site
00 000000ff`8837e1c8 00007ffd`96229ce3     KERNEL32!ReadFile
01 000000ff`8837e1d0 00007ffd`96411a4a     System_Private_CoreLib!Interop.Kernel32.ReadFile+0xa3 [/_/src/coreclr/System.Private.CoreLib/Microsoft.Interop.LibraryImportGenerator/Microsoft.Interop.LibraryImportGenerator/LibraryImports.g.cs @ 6797] 
02 000000ff`8837e2d0 00007ffd`96411942     System_Private_CoreLib!System.IO.RandomAccess.QueueAsyncReadFile+0x8a
03 000000ff`8837e350 00007ffd`96433677     System_Private_CoreLib!System.IO.RandomAccess.ReadAtOffsetAsync+0x112 [/_/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs @ 238] 
04 000000ff`8837e3f0 00007ffd`9642d5f8     System_Private_CoreLib!System.IO.Strategies.OSFileStreamStrategy.ReadAsync+0xb7 [/_/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs @ 290] 
0:000> uf 00007ffd`96229ce3
...
 6797 00007ffd`96229c98 4c8b7d30        mov     r15,qword ptr [rbp+30h]
 6797 00007ffd`96229c9c 4c897c2420      mov     qword ptr [rsp+20h],r15
 6797 00007ffd`96229ca1 498bce          mov     rcx,r14
 6797 00007ffd`96229ca4 48894dac        mov     qword ptr [rbp-54h],rcx
 6797 00007ffd`96229ca8 488bd3          mov     rdx,rbx
 6797 00007ffd`96229cab 488955a4        mov     qword ptr [rbp-5Ch],rdx
 6797 00007ffd`96229caf 448bc6          mov     r8d,esi
 6797 00007ffd`96229cb2 448945b4        mov     dword ptr [rbp-4Ch],r8d
 6797 00007ffd`96229cb6 4c8bcf          mov     r9,rdi
 6797 00007ffd`96229cb9 4c894d9c        mov     qword ptr [rbp-64h],r9
 6797 00007ffd`96229cbd 488d8d40ffffff  lea     rcx,[rbp-0C0h]
 6797 00007ffd`96229cc4 ff159e909e00    call    qword ptr [System_Private_CoreLib!Interop.CallStringMethod+0x5ab9c8 (00007ffd`96c12d68)]
 6797 00007ffd`96229cca 488b055708a100  mov     rax,qword ptr [System_Private_CoreLib!Interop.CallStringMethod+0x5d3188 (00007ffd`96c3a528)]
 6797 00007ffd`96229cd1 488b4dac        mov     rcx,qword ptr [rbp-54h]
 6797 00007ffd`96229cd5 488b55a4        mov     rdx,qword ptr [rbp-5Ch]
 6797 00007ffd`96229cd9 448b45b4        mov     r8d,dword ptr [rbp-4Ch]
 6797 00007ffd`96229cdd 4c8b4d9c        mov     r9,qword ptr [rbp-64h]
 6797 00007ffd`96229ce1 ff10            call    qword ptr [rax]
 6797 00007ffd`96229ce3 8bd8            mov     ebx,eax

仔细阅读卦中的汇编代码,通过这句
r15,qword ptr [rbp+30h]
可知 pNativeOverlapped 是保存在
r15
寄存器中。


0:000> r r15
r15=00000241ca2d4d70
0:000> dp 00000241ca2d4d70
00000241`ca2d4d70  00000000`00000000 00000000`00000000
00000241`ca2d4d80  00000000`00000000 00000000`00000000
00000241`ca2d4d90  00000000`00000001 00000241`c8761358

根据上面的模型图,
00000241ca2d4d90
保存的是引用计数,
00000241c8761358
就是我们的
ThreadPoolBoundHandleOverlapped
,可以 !do 它一下便知。

最后用 dnspy 在
Overlapped.GetOverlappedFromNative
方法中下一个断点,这个方法会在异步处理完成后,执行NativeOverlapped寻址ThreadPoolBoundHandleOverlapped 的逻辑,截图如下,那个 ReadAsync保存在内部的 _continuationState 字段里。

三:总结

C#的传统做法大多都是采用传参数的方式来建议映射关系,而本篇中用 malloc 开辟一块私有区域来映射两者的关系也真是独一份,实属无奈!
图片名称