虚幻引擎中ASan相关机制

简介

在排查一些内存问题(野指针、越界访问等)时,可以使用AddressSanitizer(ASan)工具。

比起修改为Stomp分配器的方式,ASan的实现以性能降低约一半的代价,也能扫描出来很多问题。

性能指标参考:AddressSanitizerPerformanceNumbers · google/sanitizers Wiki

Linux下DS使用

基本流程

考虑开启 ASan 对流程和性能的影响,一般需要单独部署一些炮灰环境。

  1. 在原有的打包参数之后添加-EnableAsan标识,用来开启ASan相关的编译参数
  2. 启动时通过设置环境变量ASAN_OPTIONS来控制ASan的相关配置
  3. 在原先的启动参数之后加上-ansimalloc标识
  4. 当遇见异常时,检查stderr的输出
  5. 可以选择配置log_path来指定异常时的输出文件

堆栈获取

通过指定ASAN_SYMBOLIZER_PATH可以使得输出的信息里自带堆栈,使用了llvm- symbolizer ,性能比起addr2line可以有较高的提升。

详见:AddressSanitizerCallStack · google/sanitizers Wiki

会使得输出日志稍微慢几秒,目测可以接受。

可以使用格式文本设定stack_trace_format

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  //   %% - represents a '%' character;
  //   %n - frame number (copy of frame_no);
  //   %p - PC in hex format;
  //   %m - path to module (binary or shared object);
  //   %o - offset in the module in hex format;
  //   %f - function name;
  //   %q - offset in the function in hex format (*if available*);
  //   %s - path to source file;
  //   %l - line in the source file;
  //   %c - column in the source file;
  //   %F - if function is known to be <foo>, prints "in <foo>", possibly
  //        followed by the offset in this function, but only if source file
  //        is unknown;
  //   %S - prints file/line/column information;
  //   %L - prints location information: file/line/column, if it is known, or
  //        module+offset if it is known, or (<unknown module>) string.
  //   %M - prints module basename and offset, if it is known, or PC.

不过测试下来似乎未取到函数行号,判断是llvm的问题,addr2line就可以。

windows下使用llvm-symbolizer.exe也可以成功取到。

同时,测试下来windows下使用MinGW版本的addr2line也可以取到堆栈,但是慢到无法接收。

推荐配置

开启堆栈输出并设置日志路径:

1
export ASAN_OPTIONS="symbolize=1:print_stacktrace=1:log_path=/home/crash/ds-${版本}-asan-crash.log"

问题记录

开启ASan之后加载过慢

需要检查fast_unwind_on_malloc是否被设置为0,如果fast_unwind_on_malloc被关闭,则会极大地影响性能。

详见:Address Sanitizer  |  Android NDK  |  Android Developers

堆栈还原

写了一个golang的脚本用于快速的分析堆栈:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
package main

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"strings"
)

func convertStackToAddr2line(inputFile, outputFile, binaryPath string) error {
	infile, err := os.Open(inputFile)
	if err != nil {
		return fmt.Errorf("无法打开输入文件: %v", err)
	}
	defer func(infile *os.File) {
		err := infile.Close()
		if err != nil {
			fmt.Printf("关闭输入文件时出错: %v\n", err)
		}
	}(infile)

	outfile, err := os.Create(outputFile)
	if err != nil {
		return fmt.Errorf("无法创建输出文件: %v", err)
	}
	defer func(outfile *os.File) {
		err := outfile.Close()
		if err != nil {
			fmt.Printf("关闭输出文件时出错: %v\n", err)
		}
	}(outfile)

	scanner := bufio.NewScanner(infile)
	writer := bufio.NewWriter(outfile)
	defer func(writer *bufio.Writer) {
		err := writer.Flush()
		if err != nil {
			fmt.Printf("刷新输出文件时出错: %v\n", err)
		}
	}(writer)

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "#") {
			parts := strings.Fields(line)
			var address string
			for _, part := range parts {
				if strings.HasPrefix(part, "0x") {
					address = part
					break
				}
			}
			if address != "" {
				cmd := exec.Command("addr2line", "-e", binaryPath, address)
				output, err := cmd.Output()
				if err != nil {
					_, err := fmt.Fprintf(os.Stderr, "执行 addr2line 失败: %v\n", err)
					if err != nil {
						return err
					}
					continue
				}
				s := strings.TrimSpace(string(output))
				_, err = writer.WriteString(s + "\n")
				if err != nil {
					return err
				}
				fmt.Println(s)
			}
		} else {
			_, err := writer.WriteString(line + "\n")
			if err != nil {
				return err
			}
			fmt.Println(line)
		}
	}

	if err := scanner.Err(); err != nil {
		return fmt.Errorf("读取文件时出错: %v", err)
	}

	fmt.Printf("转换完成,结果已保存到 %s\n", outputFile)
	return nil
}

func main() {
	if len(os.Args) != 4 {
		fmt.Println("Usage: go run main.go <input_file> <output_file> <binary_path>")
		os.Exit(1)
	}

	inputFile := os.Args[1]
	outputFile := os.Args[2]
	binaryPath := os.Args[3]

	if err := convertStackToAddr2line(inputFile, outputFile, binaryPath); err != nil {
		_, err := fmt.Fprintf(os.Stderr, "错误: %v\n", err)
		if err != nil {
			return
		}
		os.Exit(1)
	}
}

参考文章

该内容采用 CC BY-NC-SA 4.0 许可协议。

如果对您有帮助或存在意见建议,欢迎在下方评论交流。

加载中...