Own Crash Minidump with Call Stack

VC 6.0调试 之 远程调试 如果你写的程序在本地编译和运行没有问题,但是发布到其他机器上运行时出现了异常,程序运行的机器上没有安装调试期,此时就需要进行远程调试了。VC 6.0 可以进行远程调试, 但是需要进行一定的环境搭建。 请看下述:1.有两台机器:A机(本地机)和 B机(远程), 其中A机装有Visual studio 6.0,B机没有安装。 2.B机需要的文件 MSVCMON.EXE,DM.DLL,MSDIS110. 阅读详情
 

screenshot

Introduction

We explain how to create your own minidump with Call Stack on crash or at any given moment. The demo program creates a dump (shown on the picture) and tries to create the system one with the MiniDumpWriteDump() function of DbgHelp.dll. System dump is not supported in systems other than Win XP without individual installation. The article shows how to interpret the own minidump, and what modifications to your project settings are needed. Our competitor's minidump produced by MiniDumpWriteDump() is also discussed in brief. We refer to the demo project throughout the article, so it could be wise to build it.

The Call Stack

Call Stack is the central part of our dump. Call Stack is a list of the following frames allocated in stack:

typedef struct STACK_FRAME
{
    STACK_FRAME *   Ebp;   //address of the calling function frame

    PBYTE   Ret_Addr;      //return address

    DWORD   Param[0];      //parameter list - could be empty

} STACK_FRAME, * PSTACK_FRAME;

Each function call adds such a frame to the list, each return from function deletes the frame. So, it is a LIFO list. The address of the current frame is in the Ebp register. Ebp register of the calling function is being saved to the frame by the called function. Parameters and return address are stored to the frame by the call statement. So, if we know the current Ebp value, we can unwind the whole list. On exception, this value is in the EXCEPTION_POINTERS structure returned by the GetExceptionInformation() call. Otherwise, we can get the current Ebp subtracting the frame size (eight bytes) from the address of the first parameter of the current function. Get_Call_Stack() fills its Str parameter with Call Stack info. This is a simplified version of the Get_Call_Stack() of the demo project.

Collapse
int WINAPI Get_Call_Stack(PEXCEPTION_POINTERS pException, PCHAR Str)
{
    CHAR    Module_Name[MAX_PATH];
    PBYTE   Module_Addr;
    int     Str_Len;
    PSTACK_FRAME  Ebp;

    if (pException)
        Ebp = (PSTACK_FRAME)pException->ContextRecord->Ebp;
    else
       // Frame address of Get_Call_Stack()

       Ebp = (PSTACK_FRAME)&pException - 1;

    Str[0] = 0;
    Str_Len = 0;

    for (int Ret_Addr_I = 0;
        (Ret_Addr_I < 20) && !IsBadReadPtr(Ebp, sizeof(PSTACK_FRAME)) &&
            !IsBadCodePtr(FARPROC(Ebp->Ret_Addr));
        Ret_Addr_I++, Ebp = Ebp->Ebp)
    {
        // Find the module by a return address inside that module

        if (Get_Module_By_Ret_Addr(Ebp->Ret_Addr, Module_Name, Module_Addr))
        {
            // Save module's address and path 

            Str_Len += wsprintf(Str + Str_Len, 
                NL "%08X  %s", 
                Module_Addr, Module_Name);
            // Save offset of return address

            Str_Len += wsprintf(Str + Str_Len, 
                NL "  +%08X", 
                Ebp->Ret_Addr - Module_Addr);

            // Save 5 parameters. We don't know the real number of parameters!

            if (!IsBadReadPtr(Ebp, sizeof(PSTACK_FRAME) + 5 * sizeof(DWORD)))
            {
                Str_Len += wsprintf(Str + Str_Len, "  (%X, %X, %X, %X, %X)",
                    Ebp->Param[0], Ebp->Param[1], 
                    Ebp->Param[2], Ebp->Param[3], Ebp->Param[4]);
            }
        }
    }

    return Str_Len;
} //Get_Call_Stack

The Get_Module_By_Ret_Addr() function finds the module by a return address inside that module, and returns its path and address. It uses the Tool Help library to list all the modules of the current process.

BOOL WINAPI Get_Module_By_Ret_Addr(PBYTE Ret_Addr, 
                PCHAR Module_Name, PBYTE & Module_Addr)
{
    MODULEENTRY32  M = {sizeof(M)};
    HANDLE  hSnapshot = NULL;

    Module_Name[0] = 0;

    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0);
    
    if ((hSnapshot != INVALID_HANDLE_VALUE) &&
        Module32First(hSnapshot, &M))
    {
        do
        {
            if (DWORD(Ret_Addr - M.modBaseAddr) < M.modBaseSize)
            {
                lstrcpyn(Module_Name, M.szExePath, MAX_PATH);
                Module_Addr = M.modBaseAddr;
                break;
            }
        } while (Module32Next(hSnapshot, &M));
    }

    CloseHandle(hSnapshot);

    return !!Module_Name[0];
} //Get_Module_By_Ret_Addr

Project Settings Modification

Since minidump is primarily needed for release version, we discuss only that version. The same settings can be recommended if you want to use the MiniDumpWriteDump() function of DbgHelp.dll. After applying all these modifications, the size and speed of your program would change only negligibly, most likely would not change at all.

The Program Database (/Zi compiler option to generate .pdb file) is needed for both our own minidump and the system one:

Project Menu >> Settings >> C/C++ Tab >> Category: General >> Debug info: Program Database

Add the /Oy- compiler optimization option (along with the existent /O1 or /O2 option) to guarantee stack frame creation for all the functions:

Project Menu >> Settings >> C/C++ Tab >> Project Options: /Oy-

screenshot

Check the following checkboxes to add Linker's debug info (/debug) and MAP file:

Project Menu >> Settings >> Link Tab >> Category: General >> Generate debug info, Generate mapfile

Add the /opt:ref and /mapinfo:lines Linker options to eliminate functions and/or data that is never referenced from the resulting executable module, and include line-number info to the MAP file:

Project Menu >> Settings >> Link Tab >> Project Options: /opt:ref /mapinfo:lines

screenshot

Finding Call Stack offset location in the Source code

We can do it in three different ways. First, two methods rely on the MAP file (MiniDump.map) created by Linker, the third method is used with debug session. Suppose we've got a crash dump shown on the picture at the beginning of the article. An exception occurred at offset 16F3 in MiniDump.exe module of our demo project.

Method 1: Line numbers table of the MAP file

The Line numbers table is located at the end of the MAP file:

Line numbers for /MiniDump.obj(C:/SED/WFun/MiniDump.cpp) segment .text

68 0001:00000000    72 0001:0000000a    74 0001:00000029    76 0001:00000037
79 0001:0000003f    83 0001:00000056    89 0001:0000006d    85 0001:00000093

Here 68 0001:00000000 means <line number> <segment number>:<offset in the segment>. So, first we need to find the segment's offset in the module. It could be done by taking into account the load address of the module and the start address of the segment:

 Preferred load address is 00400000
Address       Publics by Value                                  Rva+Base

0001:00000000 ?Get_Module_By_Ret_Addr@@YGHPAEPADAAPAE@Z         00401000
0001:000000d0 ?Get_Call_Stack@@YGHPAU_EXCEPTION_POINTERS@@PAD@Z 004010d0

The segment's start 0001:00000000 address is 00401000, and the offset is 00401000 - 00400000 = 1000. Now we are ready to return to the Line numbers table and find the line number of our offset 16F3 in the module, that corresponds to offset 16F3 - 1000 = 000006F3 in the segment:

328 0001:000006c9   329 0001:000006d5   334 0001:000006e0   335 0001:000006e3
337 0001:000006f7   338 0001:00000709   343 0001:00000710   344 0001:00000713

It's easy to figure out that 335 0001:000006e3 is the culprit.

Method 2: Map file and Assembly listing

To generate the assembly listing creation, we need to set:

Project Menu >> Settings >> C/C++ Tab >> Category: Listing Files >> Listing file type: Assembly, Machine Code, and Source

Let's first find our offset 16F3 in the Publics by Value list in the MAP file:

Address       Publics by Value                                     Rva+Base

0001:00000000 ?Get_Module_By_Ret_Addr@@YGHPAEPADAAPAE@Z            00401000
0001:000000d0 ?Get_Call_Stack@@YGHPAU_EXCEPTION_POINTERS@@PAD@Z    004010d0
0001:00000240 ?Get_Version_Str@@YGHPAD@Z                           00401240
0001:000002f0 ?Get_Exception_Info@@YGPADPAU_EXCEPTION_POINTERS@@@Z 004012f0
0001:00000580 ?Create_Dump@@YGXPAU_EXCEPTION_POINTERS@@HH@Z        00401580
0001:000006e0 ?Func_3@@YGXPAX0KH@Z                                 004016e0
0001:00000710 ?Func_2@@YGXPAX0KH@Z                                 00401710

The module's address is 400000 (see "Preferred load address" in the MAP file), so we need to find out the function that contains address 4016F3. It is ?Func_3@@YGXPAX0KH@Z 004016e0 (corresponds to Func_3 in the source code), with offset in the function 4016F3 - 004016e0 = 00013. Now we can find the function by its name in the assembly listing (MiniDump.cod) and the command with offset 00013:

?Func_3@@YGXPAX0KH@Z PROC NEAR      ; Func_3, COMDAT
  334  : {
  00000 55         push  ebp
  00001 8b ec      mov   ebp, esp

  335  :     memcpy(Addr_1, Addr_2, Size);
  00003 8b 4d 10   mov   ecx, DWORD PTR _Size$[ebp]
  00006 56         push  esi
  00007 8b 75 0c   mov   esi, DWORD PTR _Addr_2$[ebp]
  0000a 8b c1      mov   eax, ecx
  0000c 57         push  edi
  0000d 8b 7d 08   mov   edi, DWORD PTR _Addr_1$[ebp]
  00010 c1 e9 02   shr   ecx, 2
  00013 f3 a5      rep movsd

No wonder we've found the same line number 335 as with Method 1.

Method 3: Debug Session

Start the Debug session with F8, F10, or F7 key. We suppose you've done that many times before. Don't forget we're debugging the release version. If you are using MFC and want to debug its code, we'd recommend to change NDEBUG to _DEBUG in:

Project Menu >> Settings >> C/C++ Tab >> Category: General >> Preprocessor definitions:

Our task is still to find the offset 16F3 in MiniDump.exe and in MiniDump.cpp. Activate the Disassembly window:

View Menu >> Debug Windows >> Disassembly Alt+8

Since this window uses addresses (not offsets), we need to determine the module's address:

Debug Menu >> Modules... >> Module List

MiniDump.exe module's address is 400000 (we have never seen this number before), so our sought address is 004016F3. Now we are very close to our goal -- enter 004016F3 in:

Edit Menu >> Go To... Ctrl+G >> Go to what: >> Address:

Press hard the Go To button, and scroll up to the beginning of the statement:

335:      memcpy(Addr_1, Addr_2, Size);
004016E3 8B 4D 10  mov   ecx,dword ptr [ebp+10h]
004016E6 56        push  esi
004016E7 8B 75 0C  mov   esi,dword ptr [ebp+0Ch]
004016EA 8B C1     mov   eax,ecx
004016EC 57        push  edi
004016ED 8B 7D 08  mov   edi,dword ptr [Addr_1]
004016F0 C1 E9 02  shr   ecx,2
004016F3 F3 A5     rep movs    dword ptr [edi],dword ptr [esi]

By the way, if you want to find an address in a DLL, do exactly the same: get the module's address from the Module List, add the offset, and Go To there. No need for setting a break point and executing the module.

Just a few words on MiniDumpWriteDump(), DbgHelp.dll, and WinDbg.exe

Though our dump with Call Stack is quite sufficient in many cases, you may also consider using the MiniDumpWriteDump() function of DbgHelp.dll. This function is available in Win XP/2003 without additional installation, and in all other systems as a redistributable. DbgHelp.dll and WinDbg.exe (needed to analyze the dump if you are not using VS.NET) can be downloaded at Debugging Tools for Windows. Don't forget to include the SDK with dbghelp.h into the download. The system minidump is especially valuable if you want to know the value of your global variables.

In the demo program, we first check to see if MiniDumpWriteDump() is available, then call it to create a minidump with the same name as of our .exe file:

    hDbgHelp = LoadLibrary("DBGHELP.DLL");
    MiniDumpWriteDump_ = (MINIDUMP_WRITE_DUMP)GetProcAddress(hDbgHelp, 
                          "MiniDumpWriteDump");

    if (MiniDumpWriteDump_)
    {
        MINIDUMP_EXCEPTION_INFORMATION  M;
        HANDLE  hDump_File;
        CHAR    Dump_Path[MAX_PATH];

        M.ThreadId = GetCurrentThreadId();
        M.ExceptionPointers = pException;  //got by GetExceptionInformation()

        M.ClientPointers = 0;

        GetModuleFileName(NULL, Dump_Path, sizeof(Dump_Path));
        lstrcpy(Dump_Path + lstrlen(Dump_Path) - 3, "dmp");

        hDump_File = CreateFile(Dump_Path, GENERIC_WRITE, 0, 
                         NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

        MiniDumpWriteDump_(GetCurrentProcess(), GetCurrentProcessId(),
            hDump_File, MiniDumpNormal,
            (pException) ? &M : NULL, NULL, NULL);

        CloseHandle(hDump_File);
    }

Now you can run WinDbg.exe to open and interpret the MiniDump.dmp:

File Menu >> Open Crash Dump... Ctrl+D
View Menu >> Command Alt+1

Enter .ecxr (Display Exception Context Record) command, and enjoy the view:

screenshot

Summary

Proposed Call Stack creation method can be implemented in C/C++ and many other languages on x86 machines.

Feel free to modify and use the demo source code for whatever you want, and don't forget to rate the article :)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Vladimir Sedach


FPGA时序优化策略 这个是时序约束的学习资料,对工作与学习是有帮助的哦。 立即下载

相关推荐

SecureCRT与SecureFX使用教程

SecureCRT与SecureFX使用教程

dump文件自动生成及调试

0 dump文件作用 Dump文件是进程的内存镜像,可以把程序的执行状态通过调试器保存到dump文件中。主要用来在系统中出现异常或者崩溃的时候生成dump文件,然后用调试器进行调试,这样就可以把生产环境中程序崩溃后生成的dmp文件拷贝到程序员的开发机上,通过调试找到程序出错的位置。 1 dump文件生成 代码封装如下: #ifndef CREATE_DUMP_H_ #define CREATE_DUMP_H_ #pragma once #include <string> #include .

wangchenxue的博客 1695

IPMI安装操作系统中文配置文档

IPMI安装操作系统中文配置文档,服务器各相关配置详细页面

捕获异常并生成minidump文件

<br />注册如下三个函数并在异常处理函数中用MiniDumpWriteDump函数生成minidump文件<br />1、SetUnhandledExceptionFilter<br />2、_set_purecall_handler<br />3、_set_invalid_parameter_handler<br />关于2,3两个需要用如下方法获得EXCEPTION_POINTERS指针<br /> <br /> <br />#include <dbghelp.h> <br />#include <

lifemap的专栏 1166

quartus中调用modelsim步骤

在QuartusII中调用modelsim仿真步骤,TB文件还请自己写。个人操作步骤截图。

OpenClaw Windows 最详细安装指南

Windows上养龙虾:OpenClaw完整安装教程+钉钉接入,助你养只大龙虾

weixin_43405859的博客 2215

VC中如何建立一个UNICODE应用程序

Gandalf   建立一个应用程序,打开Alt+F7 settings选项,- 选择 C/C++ 选项卡,在 Preprocessor definenation 中加上 _UNICODE,- 在Link选项卡中,在Category 选择框中选择 Output,在 Entry-point symbol 编辑框中,添加 wWinMainCRTStartup确定。 注意调试 UNICODE 程

黄剑雄的专栏 1556

深入解析线性反馈移位寄存器(LFSR)的工作原理与实现

本文深入解析了线性反馈移位寄存器(LFSR)的核心原理与实现方法。通过生动的比喻和详细的步骤拆解,阐述了LFSR如何通过反馈多项式和移位操作生成伪随机序列,并对比了斐波那契型与伽罗瓦型两种硬件结构。文章还涵盖了LFSR在数字电路测试、通信扰码及快速伪随机数生成等关键应用场景,同时指出了其在密码学中的局限性,并提供了Python和C语言的高效实现代码示例。

weixin_29007243的博客 545

硬件支持:alsa-utils工具包的使用

一.alsa-utils介绍 ALSA是kernel中的一个声音驱动程序.它包括alsa核心和其他声卡的驱动. alsa-utils是alsa的一个工具包,里面包含有声卡测试和音频编辑的工具. 二.alsa-utils的安装 1.RPM包方式 Turbolinux 10.5,11版本已经包含有alsa-utils的rpm包,你可以直接安装:

crycheng的专栏 3872

PD虚拟机安装CentOS7后必做的Parallels Tools配置指南(附常见错误解决)

本文详细指导在Mac的Parallels Desktop虚拟机中安装CentOS 7后如何高效配置Parallels Tools,包括环境准备、镜像挂载、依赖问题解决及性能优化。特别针对常见错误如挂载失败、依赖缺失等提供实用解决方案,帮助用户实现主机与虚拟机间的无缝交互和显示优化。

weixin_29288313的博客 302

目标检测旋转增强源码带方向

解法1: import cv2 import math import numpy as np import os def get_degree(p1,p0): aaa = math.degrees(math.atan2((p1[1] - p0[1]), (p1[0] - p0[0]))) if aaa < 0: aaa = 360 + aaa aaa = (aaa + 90) if aaa > 360: aa...

jacke121的专栏 2086

VC6.0工程设置说明

http://blog.sina.com.cn/s/blog_4bb59dc40100fcet.html所谓:“工欲善其事,必先利其器”。如果我们精于VC开发环境,我们是不是能够做得更加游刃有余呢? 先来看一下VC的处理流程,大致分为两步:编译和连接。源文件通过编译生成了.obj文件;所有.obj文件和.lib文件通过连接生成.exe文件或.dll文件。1. 打开工程设置对话框。左边的列表可以选择

小李的专栏 7295

MFC实现简单UDP通信(附完整源码)

MFC实现简单UDP通信(附完整源码)

希望我的博客,能帮上你解决学习中工作中所遇到的问题 1113

Debug和Release之本质区别

转自 Debug 和 Release 编译方式的本质区别     Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。     Debug 和 Release 的真正秘密,在于一组编译选项。下面列出了分别针对二者的选项(当然除此之外还

Think,Make 3671

ROS2 - 乐动激光雷达 LD06

STL-06P 主要由激光测距核心,无线传电单元,无线通讯单元,角度测量单元、电机驱动单元和机械外壳组成。STL-06P 测距核心采用 DTOF 技术,可进行每秒 5000 次的测距。每次测距时,雷达发射出红外激光,激光遇到目标物体后被反射到单光子接收单元。

机器人控制与仿真应用 3496

在WSL2中配置代理(Windows)

通过按照上述步骤在WSL2中配置代理,我们可以轻松地在Windows子系统Linux中使用代理服务器访问互联网。请确保提供正确的代理服务器地址和端口号,并根据实际情况进行修改。在WSL2中使用代理是一个常见的需求,它允许我们在Windows子系统Linux中通过代理服务器访问互联网。本文将详细介绍如何在WSL2中配置代理,并提供相应的源代码示例。如果返回的IP地址与代理服务器所在地不同,说明代理正常工作。希望本文能对你有所帮助!如有其他问题,请随时向我提问。替换为实际的代理服务器地址和端口号。

FcUnity的博客 4356

使用MiniDumpWriteDump API 来生成程序的Dump

<br /><br />MiniDumpWriteDump是MS DbgHelp.dll 中一个API, 用于导出当前运行的程序的Dump. 这个dll程序系统中就有, 但是很多软件, 都在自己的安装目录下保存了这个.dll的最新的版本.<br />为了测试这个API, 参考网上一些资料, 写了一个简单的C++ 程序. 目的是当有异常发生的时候, 自动生成Dump文件供之后的分析. 有了Dump文件, 我们就可以使用WinDBG等调试器来分析异常发生时的情况. 其实这个功能很多软件都有, 比如QQ, 魔兽世

zskylark的专栏 1192

SOUL.md 人格定制的 7 个高阶技巧:Hermes Agent 精准控制 AI 行为模式

揭示 SOUL.md 的本质是行为契约而非人格设定,介绍7个围绕边界约束的高阶技巧,包括用上下文锚点(INPUT SCOPE/OUTPUT TARGET/CONSTRAINTS)替代角色描述、动态渲染锚点适配CI流程等,显著降低token消耗与人工review耗时。

weixin_42520239的博客 253

通过minidump查看crash的callstack

1.1 如何查看callstack 需要找到带debugsymbol的so 有minidump或者callstack 使用minidump_stackwalk可以解析minidump文件并生成callstack文本文件 使用addr2line可以输入一个so文件和一个地址从而解析得到symbol信息 如果没有minidump,我们自己调试得到了stack,那么

695

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

Release版本程序定位Crash位置

Carsh 一、崩溃地址+MAP文件+COD文件 1. 生成应用程序MAP文件 MAP 文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,它可以在任何地方、任何时候使用,不需要有额外的程序进行支持。而且,这是唯一能找出程序崩溃的地方的救星。   1.1 VC生成MAP文件 我们可以按下 Alt+F7 ,打开“Project Settings”选项页,选择 C/C++ 选项

b711183612的专栏 1080

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

DocumentsForiTerms2(转自: https://www.iterm2.com/)

用于平时备记

Lv's Home 1万+

修改SecureCRT显示宽度

secureCRT 改变显示宽度 每次用secureCRT登陆后sqlplus查询数据都是折行显示,即使set lines 1024参数后也没用,很不爽,今天终于搞定了。 SecureCRT v5.5.1 (build 407)  1、首先全局设置:Options - Global Options - Terminal - Appearance - Maximumcolumns 最大

Tony_Wong的专栏 1万+

VC 6.0调试 之 DLL调试

你写的DLL或者ActiveX需要调试,因为他们不能直接运行 ,所以不能够直接调试,那么可以试试下面的方法:1. 在Vc 6.0中新建一个Win32 Console Application工程(例如DLLTest),代码中调用需要调试的DLL,编译该工程,生成Debug/DLLTest.exe,关闭该工程。2. 打开VC 6.0,打开需要调试的DLL工程,点击菜单Build->Set Active

FreeWave's space 6357
上一篇: 关于定位lsass内存中的明文密码
下一篇: 一种躲避运行时代码校验的方法
iiprogram
博客等级 码龄25年 1068粉丝 1515原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值