600行代码写了个小工具,替代linux中的cd和ls,取名cdls

近来用Rust写了个小工具,核心设计思路是:用方向键在各目录间跳转。兼顾了排序、文件属性显示、文件名搜索等相关功能。

考虑到在linux中,这个活儿通常是用cd和ls完成的,这个工具取名为cdls。

用法示例:

安装

在x86-64架构中,

1
2
3
wget https://xs-upload.oss-cn-hangzhou.aliyuncs.com/cdls/release/v0.3/cdls
sudo mv cdls /usr/bin/
sudo chmod +x /usr/bin/cdls

用法

1
2
3
4
5
# 启动cdls屏幕
cdls

# 显示帮助
cdls -h

在cdls屏幕中:

  1. 用方向键即可在各目录间跳转

     方向键左             上级目录
     方向键右             下级目录
     方向键上             选择前一项
     方向键下             选择后一项
    
  2. 配置屏幕,输入以下键启动配置屏幕

     c                       列显示配置
     s                       排序配置
    
     在配置屏幕中,用方向键选择配置,用空格键选定配置,用`q`保存并退出配置屏幕
    
  3. 搜索模式

     f                        启动搜索模式
     在搜索模式中,键入关键字,匹配的文件优先显示。用`enter`退出搜索模式并跳转到目的文件。
    
  4. 退出cdls

     Enter键                 退出cdls并跳转到当前目录
    

项目URL

https://github.com/SmileXie/cdls

Rust Cheat Sheet

变量

可变与不可变

1
2
let a = 10; // 不可变
let mut b = 20; // 可变

数据类型

整型

1
let a: u64 = 10; 
长度 有符号 无符号
8bits i8 u8
16bits i16 u16
32bits i32 u32
64bits i64 u64
128bits i128 u128
与cpu架构相关 isize usize

数字时指定类型,见下例中的9u32

1
println!("9 / 2 = {} but 9.0 / 2.0 = {}", 9u32 / 2, 9.0 / 2.0);

浮点型

类型为:f32f64。浮点型默认为f64

1
2
let a: f32 = 10.0;  
let b = 11.0 // 默认f64

布尔型

类型为:bool;取值为:true and false

1
let b: bool = true;

字符与字符串

1
2
3
let c: char = 'f';  // 字符
let string_c: &str = "ace"; // 字符串切片
let string_s = String::from("hello"); // 字符串

这里的string_c为“字符串切片”

元组

定义和访问元组

1
2
3
4
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;

数组

1
2
3
4
5
let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 
// 声明一个全0数组,数据成员个数为5
let bytes = [0; 5];
// 使用数组成员
let first = days[0];

数组的两个重要特征:数组的每个元素都具有相同的数据类型。 数据类型永远不会更改。数组大小是固定的。 长度永远不会更改。

向量 Vector

与数组不同之处在于,向量的大小或长度可以随时增大或缩小。 在编译时,大小随时间更改的功能是隐式的。 因此,Rust 无法像在数组中阻止越界访问一样在向量中阻止访问无效位置。

1
2
3
4
5
6
7
8
9
10
11

let three_nums = vec![15, 3, 46];
println!("Initial vector: {:?}", three_nums);

let mut fruit = Vec::new();
fruit.push("Apple");
println!("Pop off: {:?}", fruit.pop());

// 索引
let mut index_vec = vec![15, 3, 46];
let three = index_vec[1];

哈希表

1
2
3
4
5
6
7
8
9
10
use std::collections::HashMap; 
//声明与插入元素
let mut reviews: HashMap<String, String> = HashMap::new();
reviews.insert(String::from("Ancient Roman History"), String::from("Very accurate."));
//获取键值
let book: &str = "Programming in Rust";
println!("\nReview for \'{}\': {:?}", book, reviews.get(book));

let obsolete: &str = "Ancient Roman History";
reviews.remove(obsolete);

结构体

定义结构体

1
2
3
4
// 经典结构
struct Student { name: String, level: u8, remote: bool }
// 元组结构
struct Grades(char, char, char, char, f32);

主要区别:经典结构中的每个字段都具有名称和数据类型。 元组结构中的字段没有名称。

实例化

1
2
3
4
5
6
7
8
let user_1 = Student { name: String::from("Constance Sharma"), remote: true, level: 2 };

// Instantiate tuple structs, pass values in same order as types defined
let mark_1 = Grades('A', 'A', 'B', 'A', 3.75);

println!("{}, level {}. Remote: {}. Grades: {}, {}, {}, {}. Average: {}",
user_1.name, user_1.level, user_1.remote, mark_1.0, mark_1.1, mark_1.2, mark_1.3, mark_1.4);

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 声明
enum WebEvent {
WELoad,
WEKeys(String, char),
WEClick { x: i64, y: i64 }
}

// 或

struct KeyPress(String, char);
struct MouseClick { x: i64, y: i64 }
enum WebEvent { WELoad(bool), WEClick(MouseClick), WEKeys(KeyPress) }

// 赋值
let click = MouseClick { x: 100, y: 250 };
let keys = KeyPress(String::from("Ctrl+"), 'N');

let we_load = WebEvent::WELoad(true);
// Set the WEClick variant to use the data in the click struct
let we_click = WebEvent::WEClick(click);
// Set the WEKeys variant to use the data in the keys tuple
let we_key = WebEvent::WEKeys(keys);

泛型

1
2
3
4
5
6
7
8
9
struct Container<T> {
value: T,
}

impl<T> Container<T> {
pub fn new(value: T) -> Self {
Container { value }
}
}

函数

1
2
3
4
5
6
7
fn goodbye(message: &str) {
println!("\n{}", message);
}

fn divide_by_5(num: u32) -> u32 {
num / 5
}

所有权

所有权三原则

  1. Each value in Rust has a variable that’s called its owner. (Rust中每一个变量都有一个所有者。)
  2. There can only be one owner at a time.(在任一时刻,所有者有且仅有一个。)
  3. When the owner goes out of scope, the value will be dropped.(当所有者离开其作用域后,它所拥有的数据会被释放。)

Copy Trait

凡是拥有Copy trait的数据类型,“=”都表示数据的复制而非传有权的转移。以下数据类型有Copy trait:

  • 所有整型:u32 u64等
  • 布尔型
  • 所有浮点型:f32 f64等
  • 字符型:char
  • 元组(Tuples):如果组成元组的每个成员都有Copy trait,那么此元组也有Copy trait。

引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引用
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

// 可变引用
fn main() {
let mut s = String::from("hello");
change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

引用的原则

引用的原则:

  • 任何时刻,一个变量只能有
    • 一个可变引用,或者
    • 多个不可变引用
    • 以上两点不可同时存在
  • 引用应该总是合法的

Rust编译器在以下三个条件同时满足时,会产生数据竞争,发出编译错误:

  • 两个或两个以上的pointer(包含所有者,可变引用)指向同一份数据。
  • 其中至少一个可变引用指会向空间写入数据。
  • 没有同步数据的访问机制。

手动批注生存期(lifetime annotation)

1
2
3
4
5
6
7
fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
if x.len() > y.len() {
x
} else {
y
}
}

以上代码中,x和y的生命周期有可能不一样,所以函数返回值的生命周期实际上是由两个参数里生命周期较短的那个决定的。

条件判断

loop

在断点处返回一个值

1
2
3
4
5
6
7
8
9
let mut counter = 1;
// stop_loop is set when loop stops
let stop_loop = loop {
counter *= 2;
if counter > 100 {
// Stop loop, return counter value
break counter;
}
};

for

1
2
3
4
5
6
7
8
9
let big_birds = ["ostrich", "peacock", "stork"];
for bird in big_birds.iter() {
println!("The {} is a big bird.", bird);
}

// 此代码遍历数字 0、1、2、3 和 4
for number in 0..5 {
println!("{}", number * 2);
}

while

1
2
3
4
while counter < 5 {
println!("We loop a while...");
counter = counter + 1;
}

Option与Result 枚举

原型

1
2
3
4
5
6
7
8
9
enum Option<T> {
None, // The value doesn't exist
Some(T), // The value exists
}

enum Result<T, E> {
Ok(T): // A value T was obtained.
Err(E): // An error of type E was encountered instead.
}

Result

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
#[derive(Debug)]
struct DivisionByZeroError;

fn safe_division(dividend: f64, divisor: f64) -> Result<f64, DivisionByZeroError> {
if divisor == 0.0 {
Err(DivisionByZeroError)
} else {
Ok(dividend / divisor)
}
}


fn read_file_contents(path: PathBuf) -> Result<String, Error> {
let mut string = String::new();

// Access a file at a specified path
// ---------------------------------
// - Pass variable to `file` variable on success, or
// - Return from function early if there's an error
let mut file: File = match File::open(path) {
// Corrected code: Pass variable to `file` variable on success
Ok(file_handle) => file_handle,
// Corrected code: Return from function early if there's an error
Err(io_error) => return Err(io_error),
};

// Read file contents into `String` variable with `read_to_string`
// ---------------------------------
// Success path is already filled in
// Return from the function early if it is an error
match file.read_to_string(&mut string) {
Ok(_) => (),
// Corrected code: Return from function early if there's an error
Err(io_error) => return Err(io_error),
};

// Corrected code: Return `string` variable as expected by function signature
Ok(string)
}

match

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vec!与match
let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
for &index in [0, 2, 99].iter() {
match fruits.get(index) {
Some(fruit_name) => println!("It's a delicious {}!", fruit_name),
None => println!("There is no fruit! :("),
}
}

// 仅在Option为某个值时执行print
let a_number: Option<u8> = Some(7);
match a_number {
Some(7) => println!("That's my lucky number!"),
_ => {},
}

// if let

let a_number: Option<u8> = Some(7);
if let Some(7) = a_number {
println!("That's my lucky number!");
}

Trait

Trait是一组类型可实现的通用接口。个人理解:类似于给类定义方法。

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
trait Area {
fn area(&self) -> f64;
}

struct Circle {
radius: f64,
}

struct Rectangle {
width: f64,
height: f64,
}

impl Area for Circle {
fn area(&self) -> f64 {
use std::f64::consts::PI;
PI * self.radius.powf(2.0)
}
}

impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}

可以编写一个函数,该函数接受任何实现 AsJson Trait的类型

1
2
3
4
5
fn send_data_as_json(value: &impl AsJson) {
println!("Sending JSON data to server...");
println!("-> {}", value.as_json());
println!("Done!\n");
}

迭代器

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
#[derive(Debug)]
struct Counter {
length: usize,
count: usize,
}

impl Counter {
fn new(length: usize) -> Counter {
Counter {
count: 0,
length,
}
}
}

impl Iterator for Counter {
type Item = usize;

fn next(&mut self) -> Option<Self::Item> {

self.count += 1;
if self.count <= self.length {
Some(self.count)
} else {
None
}
}
}

println

1
2
3
fn main() {
println!("The first letter of the English alphabet is {} and the last letter is {}.", 'A', 'Z');
}

derive(Debug)

通过#[derive(Debug)]语法可以在代码执行期间查看某些在标准输出中无法查看的值。 要使用 println! 宏查看调试数据,请使用语法 {:#?} 以可读的方式格式化数据。

1
2
3
4
5
#[derive(Debug)]
struct MouseClick { x: i64, y: i64 }

let m = MouseClick{ x: 20, y: 30};
println!("{:#?}", m);

todo

1
todo!("Display the message by using the println!() macro");

工具

Valgrind笔记(二):MemCheck基本原理

Valgrind的MemCheck工具,可以检测多种内存错误,它是如何做到的?

V bits

MemCheck构造出一个“虚拟CPU”。与真实CPU不同的是,虚拟CPU中的每一个bit,都有一个关联的“是否有效”bit( “valid-value” bit),用于表示它关联的bit是否有效(更确切地,应该是“是否初始化”)。以下称这个bit为V bit。

例如,当CPU从内存中加载一个4-byte int时,也同时从V-bit位图中加载32bits的V bits。当CPU把这个int写回某个地址时,这32bits的V bits也会被存回V-bit位图中。

也就是说,系统中的所有bits,都有对应的V bits。不仅仅是内存,甚至是CPU寄存器,也有它们的V bits。为了实现这样的功能,MemCheck需要为V bits提供强大的压缩能力。

仅仅是访问非法的数据,并不会直接触发MemCheck的报错。仅当这一错误影响到程序的执行结果时,才会报错。例如:

1
2
3
4
5
6
int i, j;
int a[10], b[10];
for ( i = 0; i < 10; i++ ) {
j = a[i];
b[i] = j;
}

虽然访问a[i]时,未初始化,但是它并不会报错。因为这段程序仅仅是把未初始化的a[10]复制到b[10]。并没有把这些未初始的值用于某种判断或输出。如果程序改为:

1
2
3
4
5
6

for ( i = 0; i < 10; i++ ) {
j += a[i];
}
if ( j == 77 )
printf("hello there\n");

MemCheck就会报错了。未初始化的值a[i]之和,被用于计算j并用于判断if (j == 77)

在CPU做各种运算时(加,位运算等),操作数的V bits被引用,用于计算出运算结果的V bits。但是,即使这里包含非法的V bits,MemCheck仍然不会报错。仅当以下几种情况发生时,MemCheck才会去检查V bits的有效性:

  • 运算结果被用于生成地址
  • 影响程序控制流(如条件判断)
  • 有系统调用发生

这样的机制似乎看起来过度设计了,但一旦联想到C语言中有一个常见的机制叫“结构体自动填充”,你就不会这么觉得了。以下这个例子:

1
2
3
4
5
6
struct S { int x; char c; };
struct S s1, s2;
s1.x = 42;
s1.c = 'z';
s2 = s1;
if (s2) printf("s2\n");

请问,以上代码中,是否有未初始化的赋值?答案是:有。struct S在除了x和c成员外,还有3 bytes的自动填充字段,而这部分字段是未被初始化的。作为C语言程序员,想必你不会希望MemCheck因未初始化的自动填充字段而报错。

A bits

在MemCheck构建的“虚拟CPU”之上,对应每一个数据,除了V bits外,还有表示地址有效性的Valid-address bits,简称A bits。与V bits不同的是:1)A bits仅仅针对内存中的数据存在,CPU寄存器中的数据,没有A bits。2)每一byte的内存数据,对应一bit的A bit。A bits用于表示程序是否可以合法地读写此地址的数据。

每当程序读写内存时,MemCheck会检查对应地址的A bits。如果A bits指示当前的地方访问是非法的,将抛出告警。此时,MemCheck仅仅会检查A btis,而不会修改它。

A bits是如何被构建出来的呢:

  • 进程启动时,所有全局变量的A bits被标记为“可被访问”。
  • malloc/new时,申请出来的内存,被标记为“可被访问”。它们被free时,该地址修改为“不可访问”。
  • 栈指针(stack pointer register,SP)上下移动时,A bits会被修改。在SP之上,至栈基地址之间的地址,被标记为“可被访问”。SP之下的地址,被标记为“不可访问”。
  • 系统调用时, A bits会被修改。例如:mmap把一段地址映射进进程地址空间,会被标记为“可被访问”。
  • 程序可以主动告诉MemCheck,哪些地址应该被做何种标记。

规则总结

综合A bits与V bits,MemCheck检测内存错误,会遵循以下原则:

  • 内存中的每一byte数据,都有8 bits关联的V bits和1 bit的A bits。V bits表示对应的内存数据是否被初始化;A bits表示程序对此地址空间的访问,是否合法。经过压缩,A bits + V bits通常会增加25%的内存使用。
  • 当内存读写发生时,关联的A bits被取出,用于检查地址访问的合法性。MemCheck在发生非法访问时,会抛出异常。
  • 当内存被读进CPU寄存器时,关联的V bits被读进“虚拟CPU”中;* 当CPU寄存器中的数据被写进内存时,关联的V bits也从“虚拟CPU”中被写回内存中。
  • 当数据被用于生成地址,或会影响程序控制流(如条件判断)时,或数据用于系统调用,V bits被检查。其它情况下,V bits不作检查。当发现使用未初始化的数据,MemCheck会抛出异常。
  • 一旦V bits被检查过,它们就被置为“有效”。这一手段用于避免过多地重复报错。
  • 当数据从内存中加载进CPU时,MemCheck检查 A bits的合法性,在发现非法访问时抛出异常。当发生非法访问时,V bits被强制置为“有效”(尽管A bits为“不可访问”)。这种机制用于避免过多的冗余报错。
  • MemCheck会拦截内存访问接口malloc, calloc, realloc, valloc, memalign, free, new, new[], delete, delete[],这些接口中发生系统调用时,对应地修改A bits和V bits。

参考文献

Valgrind笔记(一):安装与Quick Start

安装

基于源码安装

  • 确认Valgrind最新版本
  • 下载源码:wget https://sourceware.org/pub/valgrind/valgrind-3.17.0.tar.bz2
  • 解压:tar xvf valgrind-3.17.0.tar.bz2
  • cd valgrind-3.17.0
  • 配置: ./configure
  • 编译:make install (可能需要root权限, sudo)

基于安装包安装

Ubuntu环境:sudo apt install valgrind

Quick Start

执行一个最简单的测试:

编写一段有bug的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>

void f(void)
{
int* x = malloc(10 * sizeof(int));
x[10] = 0; // problem 1: heap block overrun
} // problem 2: memory leak -- x not freed

int main(void)
{
f();
return 0;
}

编译之(注意要编译选项要带上-g),编译出的可执行文件为test。用valgrind来执行test

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
$ valgrind ./test
==4597== Memcheck, a memory error detector
==4597== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==4597== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==4597== Command: ./test
==4597==
==4597== Invalid write of size 4
==4597== at 0x10916B: f (test.c:6)
==4597== by 0x109180: main (test.c:11)
==4597== Address 0x4a47068 is 0 bytes after a block of size 40 alloc'd
==4597== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==4597== by 0x10915E: f (test.c:5)
==4597== by 0x109180: main (test.c:11)
==4597==
==4597==
==4597== HEAP SUMMARY:
==4597== in use at exit: 40 bytes in 1 blocks
==4597== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==4597==
==4597== LEAK SUMMARY:
==4597== definitely lost: 40 bytes in 1 blocks
==4597== indirectly lost: 0 bytes in 0 blocks
==4597== possibly lost: 0 bytes in 0 blocks
==4597== still reachable: 0 bytes in 0 blocks
==4597== suppressed: 0 bytes in 0 blocks
==4597== Rerun with --leak-check=full to see details of leaked memory
==4597==
==4597== For lists of detected and suppressed errors, rerun with: -s
==4597== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

以上Valgrind给出的log中,已明确指示了错误的地方:

  • test.c 第6行,访问了一个超出malloc申请范围的地址。
  • 检测到一个40 bytes的内测泄漏。通过valgrind --leak-check=full ./test查看更详细的信息。

那么,我们就用valgrind --leak-check=full ./test 试试:

1
2
3
4
5
6
...
==179== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==179== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==179== by 0x10915E: f (test.c:5)
==179== by 0x109180: main (test.c:11)
...

Valgrind检测发现了在test.c第5行malloc的内存,没有被释放。

至此,Valgrind的简单demo就完成了。Valgrind(尤其是MemCheck tool)为C/C++程序员提供了很好的检查内存错误的工具。

一个小型项目的Makefile模板

最近发现一个很好用的Makefile模板,简单修改后几乎可以适配所有常用场景。

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
# tool macros
CC ?= gcc
CCFLAGS := # FILL: compile flags
DBGFLAGS := -g
CCOBJFLAGS := $(CCFLAGS) -c


# path macros
BIN_PATH := bin
OBJ_PATH := obj
SRC_PATH := src
DBG_PATH := debug


# compile macros
TARGET_NAME := test
TARGET := $(BIN_PATH)/$(TARGET_NAME)
TARGET_DEBUG := $(DBG_PATH)/$(TARGET_NAME)


# src files & obj files
SRC := $(foreach x, $(SRC_PATH), $(wildcard $(addprefix $(x)/*,.c*)))
OBJ := $(addprefix $(OBJ_PATH)/, $(addsuffix .o, $(notdir $(basename $(SRC)))))
OBJ_DEBUG := $(addprefix $(DBG_PATH)/, $(addsuffix .o, $(notdir $(basename $(SRC)))))


# clean files list
DISTCLEAN_LIST := $(OBJ) \
$(OBJ_DEBUG)
CLEAN_LIST := $(TARGET) \
$(TARGET_DEBUG) \
$(DISTCLEAN_LIST)


# default rule
default: makedir all


# non-phony targets
$(TARGET): $(OBJ)
$(CC) $(CCFLAGS) -o $@ $(OBJ)


$(OBJ_PATH)/%.o: $(SRC_PATH)/%.c*
$(CC) $(CCOBJFLAGS) -o $@ $<


$(DBG_PATH)/%.o: $(SRC_PATH)/%.c*
$(CC) $(CCOBJFLAGS) $(DBGFLAGS) -o $@ $<


$(TARGET_DEBUG): $(OBJ_DEBUG)
$(CC) $(CCFLAGS) $(DBGFLAGS) $(OBJ_DEBUG) -o $@


# phony rules
.PHONY: makedir
makedir:
@mkdir -p $(BIN_PATH) $(OBJ_PATH) $(DBG_PATH)


.PHONY: all
all: $(TARGET)


.PHONY: debug
debug: $(TARGET_DEBUG)


.PHONY: clean
clean:
@echo CLEAN $(CLEAN_LIST)
@rm -f $(CLEAN_LIST)


.PHONY: distclean
distclean:
@echo CLEAN $(DISTCLEAN_LIST)
@rm -f $(DISTCLEAN_LIST)

代码目录结构:

1
2
3
4
5
6
7
8
9
10
11
├── Makefile
├── bin
│ └── crudc
├── debug
├── obj
│ ├── crudc.o
│ └── test.o
└── src
├── crudc.c
├── list.h
└── test.c

使用步骤:

  • 按以上目录结构组织代码
  • Makefile中,配置好这几个变量:CC, CCFLAGS, DBGFLAGS, CCOBJFLAGS, TARGET_NAME
  • makemake debug

《开端》就是在解bug

除夕夜在看《开端》,看了前几集,就似乎了有一种熟悉的感觉。

从程序员的视角来看,《开端》就是发现了一个bug后定位根因并解决的过程。期间不断通过重现bug排除错误答案。

重现第六次时,试图对bug视而不见,选择逃避。后因bug重现概率过高被boss强行拉回继续定位。

重现第十次时,确认了bug根因来源于程序内部,并非由外部API或环境因素触发。

又经过六次重现,排除了两个嫌疑重大的逻辑。

重现到第十七次,算是定位到了直接原因。

又花了七次重现,才最终定位到根因并解决bug。