2024年3月

葡萄牙语,作为一种罗曼语族的语言,其正字法(orthography)并不使用音标系统来标记发音,而是有一套特定的拼写规则。然而,葡萄牙语中确实使用重音符号(acentos)来标记某些元音的重音(stress)或音质(quality)的变化。

葡萄牙语中使用的重音符号包括:

  1. Acute accent (agudo) - 例如: é, á, ó。这个符号用于标记重音所在的元音,并且通常表示该元音是开音节的元音,例如 "é" 发音为 /ɛ/。

  2. Circumflex accent (circunflexo) - 例如: ê, ô。这个符号也用于标记重音,但通常表示该元音是闭音节的元音,例如 "ê" 发音为 /e/。

  3. Grave accent (grave) - 在葡萄牙语中,重音符号
    grave
    主要用于表示定冠词 "a" 和介词 "a" 的融合(crase),如 "à"(到...那里)。

  4. Tilde (til) - 例如: ã, õ。这个符号表示鼻化元音,例如 "ão" 发音为 /ɐ̃w̃/。

重音符号在葡萄牙语中是重要的,因为它们可以改变词义。例如,“avô”(祖父/祖母)和“avo”(鸟类的一种)就是两个意义完全不同的词。

至于“语气”(mood),这是语法术语,指的是动词形式用来表达说话者对动作的态度,如陈述、疑问、命令等。葡萄牙语有多种语气,包括陈述语气(indicativo)、虚拟语气(conjuntivo)、命令语气(imperativo)等。

在葡萄牙语学习中,理解和正确使用这些重音符号和动词的语气是非常重要的。

一、在SQL Server中如何实现
不区分重音的模糊查询

在SQL Server中进行模糊查询时,重音符号和动词的语气不会直接影响查询语句的结构,但它们会影响查询的准确性和结果。如果您希望查询能够无视重音符号(即无论用户输入带重音的字符还是不带重音的字符,都能返回结果),您需要使用某些特定的配置或者函数来实现。

以下是一些处理带有重音符号的模糊查询的方法:

  1. 使用COLLATE子句: 您可以在查询中使用
    COLLATE
    子句,指定一个不区分重音的排序规则(Collation)。例如,使用
    Latin1_General_CI_AI
    ,其中
    CI
    表示不区分大小写(Case Insensitive),
    AI
    表示不区分重音(Accent Insensitive)。
SELECT * FROM your_table
WHERE your_column COLLATE Latin1_General_CI_AI LIKE '%texto%';
  1. 使用全文搜索: 如果您的表配置了全文索引,您可以使用全文搜索来执行查询,它通常能够更好地处理语言的复杂性,包括重音符号。
SELECT * FROM your_table
WHERE CONTAINS(your_column, '"texto"');

全文搜索的行为会根据配置的全文索引的语言设置有所不同,它可以更智能地处理语言的特性。

  1. 替换字符串中的重音符号: 在某些情况下,如果无法更改数据库的排序规则或使用全文索引,您可能需要在查询之前先将输入字符串中的带重音字符替换为不带重音的等价字符。这通常涉及到在应用程序层面进行处理,而不是在SQL查询中。

请注意,这些方法可能会影响查询性能,尤其是在大型数据集上。在实施之前,应当考虑到性能影响,并进行适当的测试。

二、.NET应用程序不改变数据库配置或依赖数据库特定功能的情况下,支持不区分重音的模糊查询

在.NET应用中,您可能需要在查询数据库之前处理字符串,以便无论用户输入带重音的字符还是不带重音的字符,您的应用程序都能返回期望的结果。下面是一个示例,展示了如何在C#中使用.NET标准库的功能来替换掉字符串中的重音符号,并构建一个不区分重音的模糊查询。

using System;
using System.Globalization;
using System.Text;

public static class StringUtils
{
public static string RemoveDiacritics(string text)
{
var normalizedString = text.Normalize(NormalizationForm.FormD);
var stringBuilder = new StringBuilder();

foreach (var c in normalizedString)
{
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
{
stringBuilder.Append(c);
}
}

return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}
}

class Program
{
static void Main()
{
string input = "ação"; // User input with accents
string query = StringUtils.RemoveDiacritics(input);

// Now 'query' variable has "acao" which is without diacritics

// Use 'query' to construct your SQL query
// Example (Note: Always use parameterized queries to prevent SQL Injection):
string sqlQuery = $"SELECT * FROM YourTable WHERE YourColumn LIKE '%{query}%'";

// Execute the SQL query against your database
// ...

Console.WriteLine(sqlQuery);
}
}

在这个示例中,
RemoveDiacritics
方法使用了.NET的
Normalize
方法来分解字符串中的每个字符到它们的基础字符和非空间标记(这里的重音符号),然后它只保留了那些不是非空间标记的字符。最后,它再次将结果字符串规范化以确保字符是以它们的组合形式出现的。

请注意,上面的SQL拼接方法仅作为示例,实际开发中应该使用参数化查询来避免SQL注入攻击。

// Assuming 'connection' is an open SqlConnection object and 'command' is a SqlCommand
string sqlQuery = "SELECT * FROM YourTable WHERE YourColumn LIKE @query";
command.CommandText = sqlQuery;
command.Parameters.AddWithValue("@query", $"%{query}%");

// Execute the command as usual

通过这种方式,.NET应用程序可以在不改变数据库配置或依赖数据库特定功能的情况下,支持不区分重音的模糊查询。

三、如果用户输入acao,希望模糊查询ação,如何实现

如果用户输入的是没有重音符号的文本(例如 "acao"),但您希望能够匹配数据库中带有重音符号的文本(例如 "ação"),那么您需要确保查询时不考虑重音符号。在SQL Server中,这可以通过使用不区分重音的collation来实现。

在.NET应用程序中,您不需要对用户的输入做任何特殊处理,因为用户输入的是没有重音符号的文本。您需要确保的是,在执行SQL查询时,数据库能够理解您希望查询不区分重音符号。

这里有一个C#代码示例,展示了如何构建这样的一个参数化查询:

usingSystem;usingSystem.Data;usingSystem.Data.SqlClient;classProgram
{
static voidMain()
{
string userInput = "acao"; //User input without accents//Define the SQL query with COLLATE string sqlQuery = "SELECT * FROM YourTable WHERE YourColumn COLLATE Latin1_General_CI_AI LIKE @query";//Assuming 'connectionString' is your connection string using (SqlConnection connection = newSqlConnection(connectionString))
{
//Create a SqlCommand using (SqlCommand command = newSqlCommand(sqlQuery, connection))
{
//Add the parameter to the command command.Parameters.Add(new SqlParameter("@query", SqlDbType.NVarChar));
command.Parameters[
"@query"].Value = $"%{userInput}%";//Open the connection connection.Open();//Execute the query using (SqlDataReader reader =command.ExecuteReader())
{
while(reader.Read())
{
//Assuming you want to read the first column of the result Console.WriteLine(reader[0].ToString());
}
}
}
}
}
}

在这个示例中,
COLLATE Latin1_General_CI_AI
子句告诉SQL Server在执行
LIKE
操作时使用不区分大小写(CI)和不区分重音(AI)的规则。这意味着即使用户输入的是"acao",查询也能够匹配"ação"。

请确保在您的数据库中使用的collation支持不区分重音的搜索。
Latin1_General_CI_AI
是一个常用的不区分重音的collation,但您应该根据自己的数据库设置来选择合适的collation。

此外,这段代码使用参数化查询,这是一个最佳实践,可以防止SQL注入攻击。您应该始终使用参数化查询来处理来自用户的输入。

周国庆

2024/3/4

树的基本概念

树的定义

树是由
\(n(n \geq 0)\)
个节点组成的有限集。当
\(n = 0\)
时,称为空树。

任意一棵非空树应满足以下两点:

(1)有且仅有一个特定的称为根的节点;

(2)当
\(n > 1\)
时,其余节点可分为
\(m(m>0)\)
个互不相交的有限集
\(T_1, T_2, \dots, T_m\)
,其中每个集合本身又是一棵树,称为根的
子树

树有以下几个特点:

(1)
根节点
没有前驱,除根节点外所有节点有且仅有一个前驱;

(2)树中所有节点有零个或多个后继;

(3)n个节点的树中有n-1条边;

基本术语

image

(1)祖先、子孙、父亲、孩子、兄弟、堂兄弟

如图,考虑节点K,从根A到节点K的唯一路径的所有其他节点,称为节点K的
祖先
;路径上里节点K最近的节点E称为节点K的
父亲
,而K为E的
孩子
;有相同父亲节点的节点称为
兄弟
,如K和L;父亲节点在同一层的节点互为堂兄弟,如E、F、H、I、J。

(2)节点的度和树的度

树中一个节点的
孩子个数
称为
节点的度

树中节点的
最大度数
称为
树的度

(3)分支节点和叶节点

度大于0
的节点称为
分支节点
(非终端节点);
度为0
(没有孩子节点)的节点称为
叶节点
(终端节点);

(4)节点的深度、高度、层次

节点的层次从树根开始定义;

节点的深度
即节点所在的
层次

节点高度
是以该节点为根的
子树的高度

树的高度
是树中节点的
最大层数

(5)有序树和无序树

树中节点各子树从左到右是
有次序
的,不能互换,称为有序树,否则称为无序树;

(6)路径和路径长度

树中两个节点之间的
路径
是由这两个节点之间所经过的节点序列构成的,而
路径长度
是路径上所经过的边的个数;

(7)森林

森林是
\(m(m \ge 0)\)
棵互不相交的树的集合。

树的性质

(1)树的节点数n等于所有节点的度数之和加1。

证明:

如果是以层次的方式看待树,那么根节点的度,就等于第二层的节点数,第二层节点的度数之和,就等于第三层的节点数,以此类推。那么从第一层开始的每层节点度数和就等于从第二层开始的节点数之和,换言之,只是少了根节点。

(2)度为
\(m\)
的树中第
\(i\)
层至多有
\(m^{i-1}\)
个节点(
\(i \ge 1\)

证明:

(数学归纳法及简单推导)

第一层:至多有一个节点(根节点);

第二层:至多有m个节点(仅有两层:根节点孩子节点个数=度数m;超过两层:根节点度数大于其孩子节点度数,第二层有m个节点;根节点度数小于其孩子节点度数,树的度数为孩子节点度数m,第二层有空缺);

第三层:至多有
\(m^2\)
个节点;

根据数学归纳法,易得第n层有
\(m^{i-1}\)
个节点。

(3)高度为
\(h\)

\(m\)
叉树至多有
\((m^h-1)/(m-1)\)
个节点

证明:

(首先说明,这个性质在
\(h>1\)
时才成立)

由性质2,节点最多,则每层都有
\(m^{i-1}\)
个节点,则节点总数:

\[n = m^0+m^1+m^2+\dots+m^{h-1} = \frac{m^h-1}{m-1}。
\]

(4)度为
\(m\)
、具有
\(n\)
个节点的树的最小高度
\(h\)

\(\lceil \log_m(n(m-1)+1) \rceil\)

证明:

高度最小,则每个节点的度都要达到
\(m\)
。由性质3:
\(n = \frac{m^h-1}{m-1}\)
,

整理可得:
\(m^h - 1 = n \times (m - 1) \Longrightarrow h = \log_m(n(m-1)+1)\)

由于多余节点也是一层,向上取整,得到最小高度

(5)满
\(m\)
叉树节点编号
\(i\)
的第
\(k\)
个孩子节点编号为
\((i-1)m+k+1(1 \le k \le m)\)

证明:

设节点
\(i\)
为该$ m$ 叉树的第 $h
\(层\)
(h=1,2,3…)$,
则前
\(h-1\)
层共有
\(N_1 = \frac{m^{h-1}-1}{m-1}\)
个节点,前
\(h\)
层共有
\(N_2 = \frac{m^h-1}{m-1}\)
个节点,显然
\(i\)
为第
\(h\)
层的第
\(i-N_1\)
个节点,

\(\Longrightarrow i\)

\(i-N_1-1\)
个左兄弟

\(\Longrightarrow i\)
的第一个孩子
\(j\)

\((i-N_1-1)m\)
个左兄弟,在第
\(h+1\)
层的次序为
\((i-N_1-1)m+1\)
,在树中的编号为
\(N_2+(i-N_1-1)m+1\)

\[\begin{aligned}
& N_2 = \frac{m^h-1}{m-1} \\
& j = N_2+(i-N_1-1)m+1
\end{aligned}
\]

\(\Longrightarrow\)
节点
\(i\)
第的一个孩子
\(j = (i-1)m+2\)
;

又树为m叉树,
\(\Longrightarrow i\)
的最后一个孩子
\(j = (i-1)m+2+m-1=(i-1)m+m+1\)

\(\Longrightarrow i\)
的节点的孩子节点编号为
\((i-1)m+k+1(1 \le k \le m)\)

二叉树的概念

二叉树的定义及主要特征

二叉树是一种特殊的树形结构,其特点是每个节点最多只有两棵子树且子树有左右之分,次序不能颠倒。

【注】二叉树与度为2的有序树的区别:

  • 度为2的树至少有3个节点,二叉树可以为空;
  • 度为2的有序树的孩子的左右次序是相对另一个孩子而言的,若某个节点只有一个孩子,则这个孩子就无序区分其左右次序;而二叉树无论其孩子数是否为2,均需要确定其左右次序。

几种特殊的二叉树:

  • 满二叉树:高度为
    \(h\)
    ,且有
    \(2^h-1\)
    个节点的二叉树,对满二叉树按层序编号,约定从根节点起,自上而下,自左向右,每个节点对应一个编号,对编号为
    \(i\)
    的节点,若有父亲节点,父亲节点的编号为
    \(\lfloor i/2 \rfloor\)
    ,若有左孩子,左孩子节点为
    \(2i\)
    ,若有右孩子,右孩子节点为
    \(2i+1\)
  • 完全二叉树:高度为
    \(h\)
    ,有
    \(n\)
    个节点,当且仅当每个节点都与高度为
    \(h\)
    的满二叉树中编号为
    \(1-n\)
    的节点一一对应时,称
    完全二叉树
    .
  • 二叉排序树:左子树上的所有节点的关键字均小于根节点的关键字;右子树上所有节点的关键字均大于根节点的关键字,左子树和右子树又各是一棵二叉排序树。
  • 平衡二叉树:树中任意一个节点的左子树和右子树的高度之差的绝对值不超过1。
  • 正则二叉树:树中每个分支节点都有2个孩子,即树中只有度为0或2的节点。

二叉树的性质

(1)非空二叉树的叶节点数等于度为2的结点数加一。

证明:

设度为0,1,2的节点个数分别为
\(n_0,n_1,n_2\)
,节点总数
\(n = n_0+n_1+n_2\)
.

看二叉树中的分支数,除根节点外,其余节点都有一个分支进入,设B为分支总数,则
\(n=B+1\)
。由于这些分支都是由度为1或2的节点射出的,因此有
\(B=n_1+2n_2\)

结合以上公式,得到
\(n_0=n_2+1\)

(2)非空二叉树的第
\(k\)
层最多有
\(2^{k-1}\)
个节点(
\(k \ge 1\)
)。

证明:

等比数列。

(3)高度为
\(h\)
的二叉树至多有
\(2^h-1\)
个节点(
\(h \ge 1\)
)。

证明:

(2)中等比数列求和。

(4)对于完全二叉树从上到下,从左到右的顺序依次编号
\(1,2,\dots,n\)
,则有以下关系:


  • \(i \le \lfloor n/2 \rfloor\)
    ,则节点
    \(i\)
    为分支节点,否则为叶节点,即最后一个分支节点的编号为
    \(\lfloor n/2 \rfloor\)
  • 叶节点只可能在层次最大的两层上出现。
  • 若有度为1的节点,则只可能有1个,且该节点只有左孩子而无右孩子。
  • 按层次编号后,一旦出现某节点为叶节点或只有左孩子的情况,则编号大于
    \(i\)
    的节点均为叶节点。

  • \(n\)
    为奇数,则每个分支节点都有左右孩子;若
    \(n\)
    为偶数,则编号最大的分支节点只有左孩子,没有右孩子,其他分支节点都有左、右孩子。

  • \(i>1\)
    时,节点
    \(i\)
    的父亲节点的编号为
    \(\lfloor i/2 \rfloor\)

(5)具有
\(n(n>0)\)
个节点的完全二叉树高度为
\(\lceil \log_2(n+1) \rceil\)

\(\lfloor \log_2n \rfloor+1\)

二叉树的存储结构

(1)顺序存储结构

二叉树的顺序存储是用一组连续的存储单元依次自上而下、自左向右存储完全二叉树上的节点匀速,即将完全二叉树上编号
\(i\)
的节点元素存储在一维数组下标为
\(i-1\)
的分量中。

【注】从数组下标1开始存储树中的节点,保证数组下标与节点编号一致。

image

(2)链式存储结构

顺序存储空间利用率较低,因此二叉树一般都采用链式存储结构,用链表节点来存储二叉树中的每个节点。在二叉树中,节点结构通常包括若干数据域和若干指针域,二叉链表至少包含三个域:
数据域data、左指针域lchild、右指针域rchild

image

typedef struct BiTNode {
    int data;
    struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;

二叉树的遍历及线索二叉树

二叉树的遍历

二叉树的遍历是按照某条搜索路径访问树中每个节点,使得每个节点均被访问一次,而且仅被访问一次。由于二叉树是一种非线性结构,每个节点都可能有两棵子树,因此需要寻找一种规律,以便使二叉树上的节点能排列在一个线性队列上,进而便于遍历。

遍历一棵二叉树要决定对根节点
\(N\)
,左子树
\(L\)
和右子树
\(R\)
的访问顺序。

NLR(先序遍历,PreOrder)

若二叉树为空,则什么也不做;否则,(1)访问根节点;(2)先序遍历左子树;(3)先序遍历右子树。

image

递归算法如下:

void PreOrder(BiTree T) {
	if(T != NULL) {
        visit(T);
        PreOrder(T->lchild);
        PreOrder(T->rchild);
    }
}

非递归算法:

void PreOrder(BiTree T) {
	InitStack(S);
    BiTree p = T;
    while(p || ! IsEmpty(S)) {
        if(p) {
            visit(p);
            Push(S, p);
            p = p->lchild;
        }
        else {
            Pop(S, p);
            p = p->rchild;
        }
    }
}

LNR(中序遍历,InOrder)

若二叉树为空,则什么也不做;否则,(1)中序遍历左子树;(2)访问根节点;(3)中序遍历右子树。

image

递归算法如下:

void InOrder(BiTree T) {
    if(T != NULL) {
        InOrder(T->lchild);
        visit(T);
        InOrder(T->rchild);
    }
}

非递归算法:

void InOrder(BiTree T) {
    InitStack(S);
    BiTree p = T;
    while(p || ! IsEmpty(S)) {
        if(p) {
            Push(S, p);
            p = p->lchild;
        }
        else {
            Pop(S, p);
            visit(p);
            p = p->rchild;
        }
    }
}

LRN(后序遍历, PostOrder)

若二叉树为空,则什么也不做;否则,(1)后序遍历左子树;(2)后序遍历右子树;(3)访问根节点。

image

递归算法如下:

void PostOrder(BiTree T) {
    if(T != NULL) {
        PostOrder(T->lchild);
        PostOrder(T->rchild);
        visit(T);
    }
}

层次遍历

进行层次遍历,需要借助一个队列。层次遍历的思想如下,①首先将二叉树根节点入队;②队列非空,则队头节点出队,访问该节点,若它有左孩子,则将其左孩子入队;若它有右孩子,则将其右孩子入队;③重复②,直到队列为空。

算法如下:

void LevelOrder(BiTree T) {
    InitQueue(Q);
    BiTree p;
    EnQueue(Q, T);
    while(! IsEmpty(Q)) {
        DeQueue(Q, p);
        visit(p);
        if(p->lchild != NULL)
            EnQueue(Q, p->lchild);
        if(p->rchild != NULL)
            EnQueue(Q, p->rchild);
    }
}

由遍历序列构造二叉树

以下几个例子都采用
\(Leetcode\)
来展示。

先序、中序构造二叉树

105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

class Solution {
private:
    unordered_map<int, int> index;

public:
    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return nullptr;
        }
        
        // 前序遍历中的第一个节点就是根节点
        int preorder_root = preorder_left;
        // 在中序遍历中定位根节点
        int inorder_root = index[preorder[preorder_root]];
        
        // 先把根节点建立出来
        TreeNode* root = new TreeNode(preorder[preorder_root]);
        // 得到左子树中的节点数目
        int size_left_subtree = inorder_root - inorder_left;
        // 递归地构造左子树,并连接到根节点
        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 递归地构造右子树,并连接到根节点
        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        // 构造哈希映射,帮助我们快速定位根节点
        for (int i = 0; i < n; ++i) {
            index[inorder[i]] = i;
        }
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

中序、后序构造二叉树

106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

class Solution {
    int post_idx;
    unordered_map<int, int> idx_map;
public:
    TreeNode* helper(int in_left, int in_right, vector<int>& inorder, vector<int>& postorder){
        // 如果这里没有节点构造二叉树了,就结束
        if (in_left > in_right) {
            return nullptr;
        }

        // 选择 post_idx 位置的元素作为当前子树根节点
        int root_val = postorder[post_idx];
        TreeNode* root = new TreeNode(root_val);

        // 根据 root 所在位置分成左右两棵子树
        int index = idx_map[root_val];

        // 下标减一
        post_idx--;
        // 构造右子树
        root->right = helper(index + 1, in_right, inorder, postorder);
        // 构造左子树
        root->left = helper(in_left, index - 1, inorder, postorder);
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        // 从后序遍历的最后一个元素开始
        post_idx = (int)postorder.size() - 1;

        // 建立(元素,下标)键值对的哈希表
        int idx = 0;
        for (auto& val : inorder) {
            idx_map[val] = idx++;
        }
        return helper(0, (int)inorder.size() - 1, inorder, postorder);
    }
};

中序、层序构造二叉树

BiTree Create(DataType LEVEL[], int l1, int h1, DataType LDR[], int l2, int h2) {
	if (l2 > h2)
		return NULL;
	else {
		BiTree T = (BiTNode *)malloc(sizeof(BiTNode));
        	int mark = 0;   //标识器
		int i, j;//分别指向LEVEL和LDR中数组的元素
 
		//寻找根,LEVEL中第一个与LDR中元素匹配的即为根结点
		for (i = l1; i <= h1; ++ i) {
			for (j = l2; j <= h2; ++ j) 
				if (LEVEL[i] == LDR[j]) {
					mark = 1;
					break;
				}
			}
			if (mark == 1)
				break;
		}
		T->data = LEVEL[i];     //根节点数据域
		T->lchild = Create(LEVEL, l1 + 1, h1, LDR, l2, j - 1);
		T->rchild = Create(LEVEL, l1 + 1, h1, LDR, j + 1, h2);
		return T;
	}
}

前序、后序遍历构造二叉树*

889. 根据前序和后序遍历构造二叉树 - 力扣(LeetCode)

不唯一!

class Solution {
public:
    TreeNode *constructFromPrePost(vector<int> &preorder, vector<int> &postorder) {
        int n = preorder.size();
        unordered_map<int, int> postMap;
        for (int i = 0; i < n; i++) {
            postMap[postorder[i]] = i;
        }
        function<TreeNode *(int, int, int, int)> dfs = [&](int preLeft, int preRight, int postLeft, int postRight) -> TreeNode * {
            if (preLeft > preRight) {
                return nullptr;
            }
            int leftCount = 0;
            if (preLeft < preRight) {
                leftCount = postMap[preorder[preLeft + 1]] - postLeft + 1;
            }
            return new TreeNode(preorder[preLeft],
                dfs(preLeft + 1, preLeft + leftCount, postLeft, postLeft + leftCount - 1),
                dfs(preLeft + leftCount + 1, preRight, postLeft + leftCount, postRight - 1));
        };
        return dfs(0, n - 1, 0, n - 1);
    }
};

线索二叉树

规定:若无左子树,令
\(lchild\)
指向其前驱节点;若无右子树,令
\(rchild\)
指向其后继节点;此外还需要增加两个标志域,以标识指针域指向左(右)孩子或前驱(后继)。

image

其中标志域的含义如下:

\[\begin{aligned}
ltag &= \begin{cases} 0,\qquad \text{lchild域指示节点的左孩子} \\ 1, \qquad \text{lchild域指示节点的前驱} \end{cases} \\
rtag &= \begin{cases} 0,\qquad \text{rchild域指示节点的右孩子} \\ 1, \qquad \text{rchild域指示节点的后继} \end{cases}
\end{aligned}
\]

线索二叉树的存储结构如下:

typedef struct ThreadNode {
    inr data;
    struct ThreadNode *lchild, *rchild;
    int ltag, rtag;
}ThreadNode, *ThreadTree;

中序线索二叉树的构造

通过中序遍历对二叉树线索化的递归算法如下:

void InThread(ThreadTree &p, ThreadTree &pre) {
    if(p != NULL) {
        InThread(p->lchild, pre);
        if(p->lchild == NULL) {
            p->lchild = pre;
            p->ltag = 1;
        }
        if(pre != NULL && pre->rchild == NULL) {
            pre->rchild = p;
            pre->tag = 1;
        }
        pre = p;
        TnThread(p->rchild, pre);
    }
}

通过中序遍历建立中序线索二叉树的主过程:

void CreateInThread(ThreadTree T) {
    ThreadTree pre = NULL;
    if(T != NULL) {
        InThread(T, pre);
        pre->rchild = NULL;
        pre->rtag = 1;
    }
}

中序线索二叉树的遍历

求中序线索二叉树的中序序列下的第一个节点:

ThreadNode *Firstnode(ThreadNode *p) {
    while(p->ltag == 0)
        p = p->lchild;
    return p;
}

求中序线索二叉树中节点p在中序序列下的后继:

ThreadNode *NextNode(ThreadNode *p) {
    if(p->rtag == 0)
        return Firstnode(p->rchild);
    else
        return p->rchild;
}

不含头节点的中序线索二叉树的中序遍历算法:

void Inorder(ThreadNode *T) {
	for(ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p))
        visit(p);
}

一、简介
这是我的《
Advanced .Net Debugging
》这个系列的第三篇文章。这个系列的每篇文章写的周期都要很长,因为每篇文章都是原书的一章内容(太长的就会分开写)。再者说,原书写的有点早,有些内容还是需要修正的,调试每个案例,这都是需要时间的。今天这篇文章的标题虽然叫做“基本调试任务”,但是这章的内容还是挺多的。我本来想用一篇文章把这个章节写完,我发现是不可能的,于是就分“上“和”下”用两篇来写。既然,我们要调试我们的 .Net 应用程序,那必须掌握一些调试技巧、方法和工具。我们习惯了使用 Visual Studio IDE 的调试技巧,比如:单步调试、下断点、过程调试等,但是,有些时候,VS 是使用不了的。那我们也必须学习如何使用 Windbg 的命令,在没有 VS IDE 的情况下,如何调试我们的程序,如何设置断点、恢复执行、中断执行、退出调试回话,如何为 JIT 编译的方法设置断点,如何为没有被 JIT 编译的方法设置断点,为泛型方法设置断点等等。如果我们想成为一名合格程序员,这些调试技巧都是必须要掌握的。
如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10
调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)

下载地址:可以去Microsoft Store 去下载

开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
Net 版本:.Net 8.0
CoreCLR源码:
源码下载


说明一下,这个系列内容安排有些变动,我把基础知识和眼见为实放在了一起,讲什么内容,立刻就将讲的内容做一个眼见为实验证,这样做更便于大家理解,我认为这样会更好一些,不用在文章里来回跑了。
命令行调试器要想成功使用,必须先安装 MSVC,想要了解详情,可以去微软的官网:
https://learn.microsoft.com/zh-cn/cpp/build/building-on-the-command-line?view=msvc-170
,如果我们使用的 Visual Studio 2022,本身也有命令行工具,我们就可以直接使用。安装如图:


二、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
2.1、ExampleCore_3_1_1

1 namespaceExampleCore_3_1_12 {3     internal classProgram4 {5         static void Main(string[] args)6 {7             Console.WriteLine("Welcome to Advanced .Net Debugging!");8 Console.Read();9 }10 }11 }


2.2、ExampleCore_3_1_2

1 usingSystem.Diagnostics;2 
3 namespaceExampleCore_3_1_24 {5     internal classProgram6 {7         static void Main(string[] args)8 {9             Console.WriteLine("第一次执行,并开始中断执行!");10 Debugger.Break();11             Console.WriteLine("第二次执行,并开始中断执行!");12 Debugger.Break();13             Console.WriteLine("第三次执行,并开始中断执行!");14 Debugger.Break();15 
16             Console.WriteLine("恢复执行调试完毕!");17 Console.ReadLine();18 }19 }20 }


2.3、ExampleCore_3_1_3

1 usingSystem.Diagnostics;2 
3 namespaceExampleCore_3_1_34 {5     internal classProgram6 {7         static void Main(string[] args)8 {9             Sum1(10);10 Debugger.Break();11 
12             int i = 10;13             int j = 20;14 
15             var sum =Sum1(i);16             Console.WriteLine($"sum={sum},i={i},j={j}");17 
18 Console.ReadLine();19 }20 
21         private static int Sum1(inta)22 {23             var i =a;24             var j = 11;25             int sum =Sum2(i, j);26 
27             returnsum;28 }29 
30         private static int Sum2(int a, intb)31 {32             var i =a;33             var j =b;34             var k = 13;35 
36             var sum =Sum3(i, j, k);37             returnsum;38 }39 
40         private static int Sum3(int i, int j, intk)41 {42             return i + j +k;43 }44 }45 }


2.4、ExampleCore_3_1_4

1 namespaceExampleCore_3_1_42 {3     internal classProgram4 {5         static void Main(string[] args)6 {7             //第一次调用函数
8             Console.WriteLine("Press any key(1st instance function)");9 Console.ReadKey();10             BreakPoint bp = newBreakPoint();11             bp.AddAndPrint(10, 5);12 
13             //第二次调用函数
14             Console.WriteLine("Press any key(2nd instance function)");15 Console.ReadKey();16             bp = newBreakPoint();17             bp.AddAndPrint(100, 50);18 }19 }20 
21     internal classBreakPoint22 {23         public void AddAndPrint(int a, intb)24 {25             int res = a +b;26             Console.WriteLine("Adding {0}+{1}={2}", a, b, res);27 }28 }29 }


2.5、
ExampleCore_3_1_5

1 usingSystem.Diagnostics;2 
3 namespaceExampleCore_3_1_54 {5     internal classProgram6 {7         static void Main(string[] args)8 {9 Debugger.Break();10 
11             var mylist = new MyList<int>();12 
13             mylist.Add(10);14 
15 Console.ReadLine();16 }17 }18 
19     public class MyList<T>
20 {21         public T[] arr = new T[10];22 
23         public voidAdd(T t)24 {25             arr[0] =t;26 }27 }28 }


三、基础知识和眼见为实
3.1、调试器以及调试目标
A、知识介绍
在任何调试中都包含两个组件:调试器和调试目标。
调试器
:是一个引擎,我们必须通过这个引擎和调试目标进行交互。所有与调试目标之间的交互操作(如:设置断点、观察状态等),都可以通过调试器的命令完成,而调试器将在调试目标的环境中执行这些命令。
调试目标
:一般指我们编写的程序,对于 .Net程序员来说就是,或者是要调试的程序。
它们之间的关系,有一张图可以更好的表现他们之间的关系。如图:

B、眼见为实
在【眼见为实】这个章节里,有些调试动作是一样的,我就不每个节点都写了。我就写在这里了。首先编译好自己的项目,根据自己的喜好,可以切换到编译项目目录下,也可以直接输入项目所在目录,接着就可以进行项目调试了。
我使用的命令行工具是【Developer Command Prompt for VS 2022】


1)、使用NTSD调试器
调试源码:ExampleCore_3_1_1
我们命令行工具中输入命令:ntsd,打开新窗口。效果如图:

NTSD 的新窗口。

如果没有指定任何参数,只能显示一组可用的选项。我们将我们的项目完整路径和项目名称作为输入参数。执行【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\ExampleCore_3_1_1.exe】,弹出新窗口,如图:

新的 NTSD 窗口,如图:

上面这个截图分三个部分:第一部分是符号文件搜索路径,如图:

第二部分:加载的所有模块,表示应用程序所需要的模块都已经加载完毕。如图:

第三部分:中断指令异常。每当调试器启动一个进程或者调试器附加到一个进程的时候,调试器都会注入一个中断指令,这条指令将使调试目标停止运行。
断点指令的作用:使用户与调试器和调试目标进行交互。这里是 int 3 中断。如图:

调试器的命令提示符是:X:Y>,X 表示当前正在被调试的活动目标(在大多数调试器中,这个值为 0),Y 表示导致调试器中断的线程 ID。如图:


2)、使用 NTSD 附加进程
调试源码:ExampleCore_3_1_1
当在调试器下启动有问题的引用程序的时候,这种调试方式很有作用。如果是引用程序已经运行起来了,那我们该如何调试呢?我们可以通过给【ntsd】命令,加上 -p 命令,就可以附加进程了。比如:你写的一个 Web 服务已经成功运行起来了。随着时间的推移,这个 Web 服务开始表现出一些怪怪的行为,你希望当程序具有这总奇怪行为的时候对它进行调试,附加调试就可以大展拳脚了。
-p 参数告诉调试器希望调试一个正在运行的进程。对于这个参数后,再跟上要调试进程的 Id 就可以了。
执行命令【ntsd -p 14624】,如图:

打开新的调试器窗口,如图:

下面很有很多内容,就不显示了。


3)、使用 TList.exe 显示进程 Id。
调试源码:ExampleCore_3_1_1
如果我们想获取一个进程的 id,可以有很多方法,我们可以使用 Windows 调试工具集中的 tlist.exe,tlist.exe 会输出所有运行的进程名称和 ID。启动我们的 ExampleCore_3_1_1.exe,在控制台输出:Welcome to Advanced .Net Debugging!,我们打开命令工具【Developer Command Prompt for vs 2022】,输出命令 tlist,显示如下:

我们进程的信息如下:


4)、使用 Windbg 调试
调试源码:ExampleCore_3_1_1
编译好我们的项目,打开【Windbg Preview】调试器。依次点击【文件】--->【Launch executable】加载我们的项目文件:ExampleCore_3_1_1.exe,选择【打开】按钮,成功加载并进入调试器界面。

点击【文件】按钮,切换界面。

点击【Launch executable】按钮打开新窗口。如图:

进入调试器界面,如图:

我们就可以在下方的命令框中输入命令,调试程序了。如图:

以上是在调试器中启动有问题的引用程序的流程,如果想使用【Windbg Preview】附加进程该怎么办呢?其实,也很简单,编译项目,打开调试器,依次点击【文件】--->【Attach to process】,如图:

打开选择进程的窗口,在右侧。

之后就是进入调试器界面,使用方法就一样了。


3.2、符号
A、知识介绍
符号文件:
是一种辅助数据,它包含了对引用程序代码的一些标注信息,这些信息在调试过程中非常有用。如果没有这些辅助数据,那获得的信息只有引用程序的二进制文件了。对二进制代码进行调试是非常困难的,因为你无法看到代码中的函数名、数据结构名等。符号文件的扩展名通常是 .pdb。
在符号文件中包含很多重要的信息,例如:行号和局部变量的名称等,它能极大的提高调试的效率。
符号文件有两种类型:私有符号文件(Private)和公有符号文件(Public)。
私有符号文件
:是大多数开发人员在日常工作中使用的符号文件,其中包含了调试会话中所需要的所有符号信息。
私有符号文件一般是和我们编译的程序存放在一起的,调试器在开始调试的时候,会自动加载他们
。如图:


公有符号文件
:这类型的符号文件只是有选择的包含了一些符号信息,这会使调试工作困难一点。比如:在 Microsoft 符号服务器上存储了一些公有符号文件。每当将调试器指向 Microsoft 符号服务器时,都可以下载这些符号文件,并在调试会话中使用它们。
之所以有【私有符号文件】和【公有符号文件】之分,主要是为了保护知识产权。私有符号中包含大量底层技术信息,就很容易对应用程序进行逆向工程。公有符号就不存在这样的问题,既可以调试,又不会泄露核心技术信息。


B、眼见为实
.sympath(+) 命令的使用,加号表示不会替换,而是追加。
调试源码:ExampleCore_3_1_1
编译好我们的项目,打开【Windbg Preview】工具,依次打开【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_1.exe,进入调试器。直接执行【.sympath】命令,就可以看到当前的符号文件信息。

1 0:000>.sympath2 Symbol search path is: srv*
3 Expanded Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols
4 
5 ************* Path validation summary **************
6 Response                         Time (ms)     Location7 Deferred                                       srv*

【.sympath】命令可以设置符号文件路径。在命令后跟上具体的符号文件所在的地址。

1 0:007> .sympath E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\2 Symbol search path is: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\3 Expanded Symbol search path is: e:\visual studio 2022\source\projects\advanceddebug.netframework.test\examplecore_3_1_1\bin\debug\net8.0\4 
5 ************* Path validation summary **************
6 Response                         Time (ms)     Location7 OK                                             E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\

虽然,我们设置新的符号文件的路径,但是并不会从这个路径中加载任何符号。如果想要加载符号信息,必须执行【.reload】命令。我们还是设置回去吧,防止以后有错误。

1 0:007> .sympath srv*
2 Symbol search path is: srv*
3 Expanded Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols
4 
5 ************* Path validation summary **************
6 Response                         Time (ms)     Location7 Deferred                                       srv*

如果我们使用【.sympath】命令设置错了符号文件地址,使用【.reload】也无法加载符号文件,我们可以使用【.symfix】命令,修复问题就可以了。

1 0:007>.symfix2 DBGHELP: Symbol Search Path: cache*;SRV*https://msdl.microsoft.com/download/symbols
3 SYMSRV:  BYINDEX: 0x17
4 C:\ProgramData\Dbg\sym5 ntdll.pdb6 63E12347526A46144B98F8CF61CDED7917 SYMSRV:  PATH: C:\ProgramData\Dbg\sym\ntdll.pdb\63E12347526A46144B98F8CF61CDED791\ntdll.pdb8 SYMSRV:  RESULT: 0x00000000
9 DBGHELP: ntdll - publicsymbols10 C:\ProgramData\Dbg\sym\ntdll.pdb\63E12347526A46144B98F8CF61CDED791\ntdll.pdb11 SYMSRV:  BYINDEX: 0x18
12 C:\ProgramData\Dbg\sym13 kernel32.pdb14 85A257DB4B7B82F2E19AD96AB7BB116A115 SYMSRV:  PATH: C:\ProgramData\Dbg\sym\kernel32.pdb\85A257DB4B7B82F2E19AD96AB7BB116A1\kernel32.pdb16 SYMSRV:  RESULT: 0x00000000
17 DBGHELP: KERNEL32 - publicsymbols18         C:\ProgramData\Dbg\sym\kernel32.pdb\85A257DB4B7B82F2E19AD96AB7BB116A1\kernel32.pdb

这里有这么多输出,是因为我执行了【
!sym noisy
】命令,开启了显示符号加载的详细信息,如果不想显示,可以使用【!sym quiet 】命令。

1 0:007> !sym quiet2 quiet mode -symbol prompts on3 0:007> .symfix

【.symfix】命令后面可以跟一个参数,就是本地路径,用来缓存下载的符号文件,就不用调试器每次去下载相同的符号了,可以直接从本地加载。在默认情况下,如果没有指定本地路径缓存,那么调试器将使用调试软件包安装的路径下的 sym 文件夹。

3.3、控制调试目标的执行
在任何调试会话中,能够控制调试目标的执行是非常重要的。我们可以设置断点,然后恢复程序的执行知道断点处,在此可以查看应用程序的状态,单步跟踪到函数内部,然后在恢复执行等。

3.3.1、中断执行
调试器中断程序的方式有很多种,我举三种最常用的中断执行的方式。
1)、如果我们使用的命令行调试器,可以使用【ctrol+c】组合键手动方式中断调试目标的执行,例如:调试死锁问题。
2)、给我们的应用程序设置断点来中断调试目标的执行。通过设置断点,可以很方便的使调试器在执行流程的任意位置上中断执行。
3)、抛出异常可以使调试器中断执行。

3.3.2、恢复执行
A、知识介绍
当调试器中断执行时(可能是触发了断点或者其他的事件),可以使用【g】命令回复调试器的执行。如果【g】命令不带任何参数,只是回复调试目标的执行,直到下一次发生某个调试事件。
B、眼见为实
1)、使用【g】命令恢复执行。
调试源码:ExampleCore_3_1_2
编译好我们的项目,打开我们的命令工具,输入命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】,打开调试器新窗口。

打开新的调试器窗口,如图:

触发初始断点是调试器的默认行为,也是调试人员开始分析应用程序的最早时机。此时,调试目标会停止执行并等待输入命令。此刻,我们可以输入【g】命令,恢复执行,调试器输出如图:

2)、禁用初始和退出断点
调试源码:ExampleCore_3_1_2
如果不希望调试器在初始启动时停止程序的执行,可以在启动调试器时使用 -g 命令开关,每当调试器退出时,调试器也将停止执行,可以使用 -G(大写)命令开关,避免在进程结束时触发最终的断点。
编译好我们的项目,打开我们的命令行工具,使用【ntsd -g E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】命令,启动调试器。如图:

打开新的调试器窗口,这次已经输出“第一次执行,并开始中断执行!”,如图:

如果我们使用 -G 命令开关,执行命令【ntsd -G E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】,调试器在初始会中断执行。如图:

如果没有使用 -G 命令开关,我们还需要输入命令才能退出。

3.3.3、单步调试代码
我们使用过 VS IDE 的调试功能,快捷键有:F10,F11,F9等,调试器也为我们提供了类似的命令。但是,
需要注意:如果在调试托管代码时使用非托管调试器,那么通常是对 JIT 编译器产生的机器代码进行单步调试。
A、知识介绍
1)、p 命令


p(step):命令其实就是 VS 中的 f10 快捷键,单步执行,遇到函数也是当成一条指令执行,不会进入函数体。
2)、t 命令


t(trace):命令其实就是 VS 的 f11 快捷键,它是一种进入函数的单步执行调试。

3)、pc 命令


pc(Step to Next Call)    就是一直运行直到遇到 call 为止,不会进入函数体,call 是一个函数调用,汇编指令。


4)、tc 命令


tc(Trace to Next Call)    和 pc 不同的是,tc 会进入方法体,直到遇到 call 为止。


5)、pt 命令


pt(Step to Next Return)    如果有方法会进入方法内部递归处理,遇到下一个 ret 为止。


6)、tt 命令


tt(Trace to Next Return)    会进入函数体直到遇到 ret 为止。递归的意思。


B、眼见为实
这一节的演示,使用【Windbg Preview】,我没有使用的是【NSTD】调试器,其他他们是一样的,只不过一个是由界面的,一个是没界面的。有些操作是一样的,我就写在这里了。
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_3.exe,进入调试器。界面的内容太多,我们可以使用【.cls】命令,清空调试器的界面。我们再使用【g】命令,继续运行调试器,我们现在查看一下托管代码的调用栈,执行命令【!clrstack】。

0:000>g
ModLoad: 00007ff9`454c0000 00007ff9`454f0000 C:\Windows\System32\IMM32.DLL
ModLoad: 00007ff8`8d8e0000 00007ff8`8d938000 C:\Program Files\dotnet\host\fxr\
8.0.0\hostfxr.dll
ModLoad: 00007ff8`812d0000 00007ff8`
81334000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\hostpolicy.dll
ModLoad: 00007ff8`80de0000 00007ff8`812cb000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.0\coreclr.dll
ModLoad: 00007ff9`454f0000 00007ff9`
45619000C:\Windows\System32\ole32.dll
ModLoad: 00007ff9`44b10000 00007ff9`44e64000 C:\Windows\System32\combase.dll
ModLoad: 00007ff9`45d60000 00007ff9`45e35000 C:\Windows\System32\OLEAUT32.dll
ModLoad: 00007ff9`444f0000 00007ff9`4456f000 C:\Windows\System32\bcryptPrimitives.dll
(
3994.1028): Unknown exception - code 04242420(first chance)
ModLoad: 00007ff8`7fb10000 00007ff8`807a8000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.0\System.Private.CoreLib.dll
ModLoad: 00007ff8`7f950000 00007ff8`7fb08000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.0\clrjit.dll
ModLoad: 00007ff9`
44120000 00007ff9`44133000C:\Windows\System32\kernel.appcore.dll
ModLoad: 0000021a`398c0000 0000021a`398c8000 E:\Visual Studio
2022\.\ExampleCore_3_1_3\bin\Debug\net8.0\ExampleCore_3_1_3.dll
ModLoad: 0000021a`398d0000 0000021a`398de000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.0\System.Runtime.dll
ModLoad: 00007ff8`7f920000 00007ff8`7f948000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\
8.0.0\System.Console.dll
(
3994.1028): Break instruction exception - code 80000003(first chance)
KERNELBASE
!wil::details::DebugBreak+0x2:
00007ff9`
44799202 cc int 3 0:000> !clrstack
OS Thread Id:
0x1028 (0)
Child SP IP Call Site
00000020B9B7E6A8 00007ff944799202 [HelperMethodFrame: 00000020b9b7e6a8] System.Diagnostics.Debugger.BreakInternal()
00000020B9B7E7B0 00007ff87ff360aa System.Diagnostics.Debugger.Break() [
/_/src/coreclr/./Debugger.cs @ 18]
00000020B9B7E7E0 00007ff8213f197f ExampleCore_3_1_3.Program.Main(System.String[]) [E:\Visual Studio
2022\.\ExampleCore_3_1_3\Program.cs @ 10]

我们找到了红色标注的【Program.Main()】方法的地址:
00007ff8213f197f

有了这个地址,我们就可以对这个地址下一个断点。

1 0:000> bp 00007ff8213f197f

设置好断点后,我们就可以使用【g】命令,继续运行调试器。


1)、p、pc、pt 命令的使用
调试源码:ExampleCore_3_1_3
我们设置好了断点,就可以开始我们的调试工作了。继续使用【g】运行调试器,调试器会在【Debugger.Break()】这行代码暂停,效果如图:

我们可以使用【p】命令,单步执行,到了第15行代码,会直接跳过而执行,不会进入方法。当然,这个过程要执行多次【p】命令。

1 0:000>p2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x50:3 00007ff8`213f1980 c745fc0a000000  mov     dword ptr [rbp-4],0Ah ss:00000020`b9b7e84c=00000000
4 0:000>p5 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x57:6 00007ff8`213f1987 c745f814000000  mov     dword ptr [rbp-8],14h ss:00000020`b9b7e848=00000000
7 0:000>p8 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x5e:9 00007ff8`213f198e 8b4dfc          mov     ecx,dword ptr [rbp-4] ss:00000020`b9b7e84c=0000000a10 0:000>p11 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x61:12 00007ff8`213f1991 ff1531520a00    call    qword ptr [00007ff8`21496bc8] ds:00007ff8`21496bc8=00007ff8213f1a2013 0:000>p14 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x67:15 00007ff8`213f1997 8945c0          mov     dword ptr [rbp-40h],eax ss:00000020`b9b7e810=00000000
16 0:000>p17 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x6a:18 00007ff8`213f199a 8b4dc0          mov     ecx,dword ptr [rbp-40h] ss:00000020`b9b7e810=00000022
19 0:000>p20 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x6d:21 00007ff8`213f199d 894df4          mov     dword ptr [rbp-0Ch],ecx ss:00000020`b9b7e844=00000000
22 0:000>p23 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x70:24 00007ff8`213f19a0 488d4dc8        lea     rcx,[rbp-38h]

【pc】命令调试:
前面的操作一样,查看堆栈,设置断点,开始运行,到断点出暂停。
【pc】命令很简单,我们直接输入【pc】,代码直接会运行到【var sum = Sum1(i)】,如图:

中间的代码是直接跳过的。

【pt】命令调试:
前面的操作一样,查看堆栈,设置断点,开始运行,到断点出暂停。
我又增加了一些断点,断点如图:

接着如图:

执行【pt】命令的过程如下,执行【g】命令,到【Debugger.Break()】这样代码处中断执行。

1 0:000>g2 Breakpoint 0hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f:4 00007ff8`2101197f 90              nop

效果如图:

继续执行【pt】命令,会在【var sum = Sum1(i)】这行带出中断执行。

1 0:000>g2 Breakpoint 1hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x5e:4 00007ff8`20af198e 8b4dfc          mov     ecx,dword ptr [rbp-4] ss:000000ef`fb17e6ac=0000000a

执行效果如图:

继续执行【pt】命令,会进入【Sum1()】方法内部,在断点处中断执行。

1 0:000>pt2 Breakpoint 2hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x24:4 00007ff8`20af1a84 90              nop

执行效果如图:

继续执行【pt】命令,会到【int sum = Sum2(i, j)】这行代码中断执行。

1 0:000>pt2 Breakpoint 8hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x32:4 00007ff8`20af1a92 8b4dfc          mov     ecx,dword ptr [rbp-4] ss:000000ef`fb17e62c=0000000a

执行效果如图:

继续执行【pt】命令,会进入【Sum2()】方法内部。

1 0:000>pt2 Breakpoint 4hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x2c:4 00007ff8`20af1afc 90              nop

执行效果如图:

继续执行【pt】命令,会到【var sum=Sum3(i,j,k)】这行代码处中断执行。

1 0:000>pt2 Breakpoint 5hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x40:4 00007ff8`20af1b10 8b4dfc          mov     ecx,dword ptr [rbp-4] ss:000000ef`fb17e5dc=0000000a

执行效果如图:

继续执行【pt】命令,会进入【Sum3()】方法内部。

1 0:000>pt2 Breakpoint 13hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x27:4 00007ff8`20af1b77 90              nop

执行效果如图:

继续执行【pt】命令,执行到43行代码处中断执行。

1 0:000>pt2 Breakpoint 7hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x35:4 00007ff8`20af1b85 8b45fc          mov     eax,dword ptr [rbp-4] ss:000000ef`fb17e58c=00000022

执行效果如图:

最后我们执行一个【pt】命令,也就是【Sum3()】方法结束,遇到【ret】,调试器中断执行。

1 0:000>pt2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x3d:3 00007ff8`20af1b8d c3              ret

这里我增加了很多断点,是为了测试是否会进入方法内部。执行【pt】命令,如果有方法调用会进入方法内部,知道遇到【ret】为止。


2)、t、tc、tt 命令的使用
调试源码:ExampleCore_3_1_3
【t】命令使用:
我们进入调试器,设置断点,使用【g】命令运行调试器。调试器会在 Program.Main() 方法的【Debugger.Break()】这行代码中断执行。

1 0:000>g2 Breakpoint 0hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f:4 00007ff8`20dd197f 90              nop

执行效果如图:

继续运行【t】命令,单步执行,遇到【Sum1(i)】方法,就会进入方法内部进行单步调试。

【t】命令很简单,就像 VS 的 F11快捷键一样,按一下执行一条命令。


【tc】命令使用:
当我们在调试器中使用【bp】命令设置好断点后,就可以看是测试命令了。

1 0:000>g2 Breakpoint 0hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f:4 00007ff8`20e0197f 90              nop

执行【g】命令,调试器会在 Program.Main() 方法的【Debugger.Break()】这行代码出中断执行。执行效果如图:

我们继续执行【tc】命令,它会到【var sum = Sum1(i)】这行代码处中断执行,因为调用 Sum1方法是通过【call】指令的。

1 0:000>tc2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x61:3 00007ff8`20e01991 ff1531520a00    call    qword ptr [00007ff8`20ea6bc8] ds:00007ff8`20ea6bc8=00007ff820e01a60

执行效果如图:

再次执行【tc】命令,调试器会在Sum1方法内的【int sum = Sum2(i, j)】这行代码处中断执行。

1 0:000>tc2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x38:3 00007ff8`20e01a98 ff1542510a00    call    qword ptr [00007ff8`20ea6be0] ds:00007ff8`20ea6be0=00007ff820e01ad0

执行效果如图:

就不继续了,下一个中断执行点是Sum2方法【var sum = Sum3(i, j, k)】这行代码,这个命令很简单。


【tt】命令使用:
当我们在调试器中设置到断点后,就可以开始调试了,测试我们的命令了。
我们使用【g】命令运行调试器,调试器会在Program.Main方法的【Debugger.Break()】这行代码处中断执行。

1 0:000>g2 Breakpoint 0hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f:4 00007ff8`20e0197f 90              nop

执行效果如图:

继续执行【tt】命令,会进入Sum1方法内部。

1 0:000>tt2 Breakpoint 2hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x24:4 00007ff8`20e01a84 90              nop

执行效果如图:

再次继续执行【tt】命令,会进入Sum2方法内部。

1 0:000>tt2 Breakpoint 4hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x2c:4 00007ff8`20e01afc 90              nop

执行效果如图:

再次继续执行【tt】命令,会进入Sum3方法内部。

1 0:000>tt2 Breakpoint 6hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x27:4 00007ff8`20e01b77 90              nop

执行效果如图:

当我们再次运行【tt】命令,调试器会在【43】行中断执行。再次执行【tt】命令,遇到Sum3方法的返回命令【ret】则为止。

1 0:000>tt2 Breakpoint 7hit3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x35:4 00007ff8`20e01b85 8b45fc          mov     eax,dword ptr [rbp-4] ss:00000045`d7f7e52c=00000022
5 0:000>tt6 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x3d:7 00007ff8`20e01b8d c3              ret

这个命令也不复杂,大家慢慢体会吧。


3.3.4、退出调试回话
在执行完一个调试会话后,可以有很多方式退出调试回话,这里演示主要是以命令行调试器为主。
A、知识介绍
1)、q(quit):结束调试会话+调试程序退出


调试会话结束,应用程序也会退出。


2)、qd(quit and detach):结束调试会话+调试程序继续运行


调试会话结束,应用程序保持运行态,不会退出。
B、眼见为实
这里调试我使用的是【ntsd】,没有使用【Windbg Preview】,使用是一样的。
1)、q 命令退出
调试源码:ExampleCore_3_1_2
编译好我们的项目,执行命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】加载调试器。如图:

开启新的调试器窗口,我们可以输入【q】命令,查看结果。如图:

按回车,调试器也关闭了,程序也关了。


2)、qd 命令退出
调试源码:ExampleCore_3_1_2
编译好我们的项目,通过命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】加载调试器。如图:

打开新的调试器窗口,输入命令【qd】,按回车,如图:

效果很明显,不用多说了。


3.4、加载托管代码调试的扩展命令
3.4.1、加载 SOS 调试器扩展
A、知识介绍
SOS 调试器的扩展 DLL 与程序使用的 CLR 版本是是相关的。因此,在发布每个 CLR 主版本的同时,都会发布一个新版本的 SOS 调试扩展。以确保这个 DLL 可以使用该版本 CLR 的新功能。SOS 扩展作为运行时的一部分发布的,它的路径位于:%systemRoot%Microsoft.Net\Framework\<framework version>\sos.dll。
在非托管调试器中可以使用两类命令,
一类是:元命令,另一类是:扩展命令

元命令
:指在调试器引擎中内置的命令,当使用该命令的时候,必须在命令前加上英文点号。如:.cls。如果想要列出所有的元命令,可以使用【.help】命令。
扩展命令
:指在调试器引擎之外的独立的 dll 中实现的,这些 DLL 也被称为调试器扩展。在使用扩展命令的时候,命令前面加上前缀“!”。如:!clrstack。
无论是【NTSD】还是【Windbg Preview】,现在会自动加载 SOS.DLL,以前的老版本需要使用【.load】加载 SOS.DLL。

B、眼见为实
使用【.load】加载 SOS.DLL

1 0:000> .load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.dll


3.4.2、加载 SOSEX 调试器扩展
这个调试器扩展很好用,但是也很可惜,它只支持 Net Framework 版本。在最新的 .Net 版本是抛弃的,不能使用了。如果想查看调试过程,可以查看我的另外一个系列【Net 高级调试】中有一篇文章,地址:
https://www.cnblogs.com/PatrickLiu/p/17788840.html

3.5、控制 CLR 的调试
如果我们想在托管代码调试的过程总输出各种信息(例如:SOS 命令的输出),我们可以加载一个辅助 DLL,称为:mscordacwks.dll。加载【mscordacwks.dll】的路径取决于倍加再到进程中【mscorwks.dll】的路径。在【实时调试】中一般没有问题,两个 dll 版本是一致的。如果是【事后调试】,可能会出现版本不一致的情况,我们可以使用元命令:cordll 来解决。
比如:.cordll -lp c:\x\y\z,这样就能告诉调试器从 c:\x\y\z 目录下加载 mscordacwks.dll。

3.6、设置断点
设置断点的目的就是为了告诉目标程序在执行到了断点处停止执行。断点可以使用开发人员分析程序在执行流中的状态,并且找到出现问题的根本原因。在非托管代码中设置断点很容易,因为我们知道了代码的位置,于是就可以使用【bp】命令在代码位置处设置断点了。

3.6.1、在非托管代码中设置断点。
这次我们使用 notepad.exe 做这个调试,因为它是非托管程序,代码已经编译成机器码了,代码地址有了,我们就可以使用【bp】命令直接设置断点了。
眼见为实:
1)、使用【NTSD】调试
我们先打开【notepad.exe】应用程序,然后,执行【tlist】命令,获取 notepad 应用程序的 id,效果如图:

执行命令【ntsd -p 9580】,附加 notepad 应用程序的进程,打开调试器。

1 D:\Program Files\Microsoft Visual Studio\2022\Community>ntsd -p 9580

执行效果如图:

打开调试器窗口,如下:

这个时候,我们打开的 notepad 是不能操作的,因为调试器已经中断执行了。
执行【X notepad!*Save*】命令,查找 Notepad 的包含 Save 关键字的方法。

1 0:001> X notepad!*Save*
2 00007ff6`510e86a0 notepad!ShowOpenSaveDialog (void)3 00007ff6`510ec5cc notepad!InitLegacyOpenSaveEncodingComboBox (void)4 00007ff6`510ffd54 notepad!TraceFileSaveStart (void __cdecl TraceFileSaveStart(void))5 00007ff6`510f047c notepad!SaveGlobals (void __cdecl SaveGlobals(void))6 00007ff6`510f0314 notepad!RegGetIntSaveDefault (unsigned long __cdecl .....)7 00007ff6`510ec7a0 notepad!NpLegacySaveDialogHookProc (unsigned __int64 __cdecl ....))8 00007ff6`511013a8 notepad!FileSaveDialog_GetSelectedEnterpriseId (FileSaveDialog_GetSelectedEnterpriseId)9 00007ff6`510e8b8c notepad!InvokeLegacySaveDialog (long __cdecl InvokeLegacySaveDialog(unsigned short const *,...)10 00007ff6`510fef58 notepad!RestartHandler::TryAutosaveOpenedDocument (public: bool __cdecl RestartHandler::TryAutosaveOpenedDocument(void))11 00007ff6`510ee780 notepad!SaveFile (bool __cdecl SaveFile(struct HWND__ *,.....))12 00007ff6`510ea2e8 notepad!CheckSave (int __cdecl CheckSave(void))13 00007ff6`566666623bc notepad!fInSaveAsDlg = <no type information>
14 00007ff6`510ea124 notepad!CheckSaveTaskDlgBox (int __cdecl CheckSaveTaskDlgBox(unsigned short const *))15 00007ff6`510ffdd4 notepad!TraceFileSaveComplete (void __cdecl TraceFileSaveComplete(struct _NP_FileInfo *,int))16 00007ff6`510e8d90 notepad!InvokeSaveDialog (long __cdecl InvokeSaveDialog(struct HWND__ *,...))17 00007ff6`566666605c0 notepad!szSaveCaption = <no type information>
18 00007ff6`5110508c notepad!_imp_load_GetSaveFileNameW (__imp_load_GetSaveFileNameW)19 00007ff6`56666661640 notepad!g_ftSaveAs = <no type information>
20 00007ff6`51107570 notepad!CLSID_FileSaveDialog = <no type information>
21 00007ff6`510ff258 notepad!RestartHandler::TryRestoreAutosavedDocument (public: bool __cdecl...)22 00007ff6`566666650b0 notepad!_imp_GetSaveFileNameW = <no type information>

代码中有些【...】这样的省略号,表示内容太长,省略了。

红色标注的就是我们找到了 notepad 保存功能的方法名称和地址。我们直接执行【bp notepad!SaveFile】命令或者【bp 00007ff6`510ea2e8】命令,都可以在 SaveFile 方法上下断点。

1 0:001> bp notepad!SaveFile

下完断点后,我们【g】继续执行。但是,此时调试器的光标在闪动,我们打开的 notepad 窗口也可以使用了。效果如图:

我们在 notepad 窗口中随意写一些文字,点击【文件】-->【保存】,就会触发断点。效果如图:

此时的 notepad 应用程序的窗口是不能使用的,因为在断点出已经中断执行了。
我们继续使用【g】命令,继续调试器的运行,notepad 才可以正常使用,文件也保存成功。

2)、使用【Windbg Preview】调试。

我们先打开一个 notepad.exe 应用程序。然后再打开【Windbg Preview】,依次点击【文件】--->【Attach to process】,在窗口右侧【进程列表】框中选择 notepad 进程,点击【附加】,进入调试器。

我们使用【X notepad!*Save*】命令,查找 notepad 的保存数据的方法。

1 0:002> X notepad!*Save*
2 00007ff6`510e86a0 notepad!ShowOpenSaveDialog (void)3 00007ff6`510ec5cc notepad!InitLegacyOpenSaveEncodingComboBox (void)4 00007ff6`510ffd54 notepad!TraceFileSaveStart (void __cdecl TraceFileSaveStart(void))5 00007ff6`510f047c notepad!SaveGlobals (void __cdecl SaveGlobals(void))6 00007ff6`510f0314 notepad!RegGetIntSaveDefault (unsigned long __cdecl ...)7 00007ff6`510ec7a0 notepad!NpLegacySaveDialogHookProc (unsigned __int64 __cdecl ...)8 00007ff6`511013a8 notepad!FileSaveDialog_GetSelectedEnterpriseId (FileSaveDialog_GetSelectedEnterpriseId)9 00007ff6`510e8b8c notepad!InvokeLegacySaveDialog (long __cdecl ...)10 00007ff6`510fef58 notepad!RestartHandler::TryAutosaveOpenedDocument (public: bool __cdecl RestartHandler::TryAutosaveOpenedDocument(void))11 00007ff6`510ee780 notepad!SaveFile (bool __cdecl SaveFile(struct HWND__ *,class ...))12 00007ff6`510ea2e8 notepad!CheckSave (int __cdecl CheckSave(void))13 00007ff6`566666623bc notepad!fInSaveAsDlg = <no type information>
14 00007ff6`510ea124 notepad!CheckSaveTaskDlgBox (int __cdecl CheckSaveTaskDlgBox(unsigned short const *))15 00007ff6`510ffdd4 notepad!TraceFileSaveComplete (void __cdecl TraceFileSaveComplete(struct _NP_FileInfo *,int))16 00007ff6`510e8d90 notepad!InvokeSaveDialog (long __cdecl InvokeSaveDialog(struct HWND__ *...))17 00007ff6`566666605c0 notepad!szSaveCaption = <no type information>
18 00007ff6`5110508c notepad!_imp_load_GetSaveFileNameW (__imp_load_GetSaveFileNameW)19 00007ff6`56666661640 notepad!g_ftSaveAs = <no type information>
20 00007ff6`51107570 notepad!CLSID_FileSaveDialog = <no type information>
21 00007ff6`510ff258 notepad!RestartHandler::TryRestoreAutosavedDocument (public: bool __cdecl...)22 00007ff6`566666650b0 notepad!_imp_GetSaveFileNameW = <no type information>

红色标注的就是我们要找的方法名和地址。此时,notepad的窗口是不可以使用的。使用【
bp 00007ff6`510ee780
】命令下断点。

1 0:002> bp 00007ff6`510ee780

继续【g】,运行调试器,我们操作 notepad窗口,随意输入文字,然后点击【文件】--->【保存】,调试器运行,在 SaveFile 方法的断点出停止执行,notepad 窗口也不能使用了。

我们使用【g】命令,继续运行,notepad 保存成功。


3.6.2、在 JIT 编译的托管函数上下断点
A、知识介绍
非托管方法设置断点很容易,因为代码都已经被编译了,代码的地址就是已知的。但是,托管代码要进行两次编译才能运行。我们想要给代码设置断点,必须先找到代码的位置。这一节我们讨论已经编译的函数如何设置断点,既然已经编译了,说明代码的地址就是可以直接找到的,设置断点就很容易了。
JIT 编译器编译了一个函数并将其放在内存中。如果我们知道了 JIT 编译器保存机器代码的位置,我们就可以使用调试器命令【bp】设置断点了。
B、眼见为实
调试任务:在第二次调用 AddAndPrint 方法的时候设置断点。为什么选择第二次,第一次已经执行过了,说明已经编译了。第二次就是使用编译的机器码。
1)、使用【NTSD】调试
调试源码:ExampleCore_3_1_4
执行【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\bin\Debug\net8.0\ExampleCore_3_1_4.exe】命令,开启调试器。

打开【ntsd】调试器窗口。

我们使用【g】命令,运行调试器,直到调试器显示【Press any key(1st instance function)】暂停,等待输入。

我们按下任意键,程序继续执行,直到调试器输出【Press any key(2nd instance function)】。此时,我们按下【ctrl+c】进入调试器的中断模式。

现在,我们可以使用【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令找到方法的是否编译的信息。

1 0:002> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint2 Module:      00007ffccc24e0a03 Assembly:    ExampleCore_3_1_4.dll4 Token:       0000000006000003
5 MethodDesc:  00007ffccc2793986 Name:        ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)7 JITTED Code Address: 00007ffccc1c1a90

红色标注的说明代码已经编译了,地址是:00007ffccc1c1a90,如果不信,我们可以使用【u】命令确认一下。

1 0:002> !U 00007ffccc1c1a902 Normal JIT generated code3 ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)4 ilAddr is 000002B7069720AC pImport is000001A39F8361405 Begin 00007FFCCC1C1A90, size b76 >>> 00007ffc`cc1c1a90 55push    rbp7 00007ffc`cc1c1a91 4883ec40        sub     rsp,40h8 00007ffc`cc1c1a95 488d6c2440      lea     rbp,[rsp+40h]9 00007ffc`cc1c1a9a c5d857e4        vxorps  xmm4,xmm4,xmm410 00007ffc`cc1c1a9e c5f97f65e0      vmovdqa xmmword ptr [rbp-20h],xmm411 00007ffc`cc1c1aa3 c5f97f65f0      vmovdqa xmmword ptr [rbp-10h],xmm412 00007ffc`cc1c1aa8 48894d10        mov     qword ptr [rbp+10h],rcx13 00007ffc`cc1c1aac 895518          mov     dword ptr [rbp+18h],edx14 00007ffc`cc1c1aaf 44894520        mov     dword ptr [rbp+20h],r8d15 00007ffc`cc1c1ab3 833d6ec8080000  cmp     dword ptr [00007ffc`cc24e328],0
16 00007ffc`cc1c1aba 7405je      00007ffc`cc1c1ac117 00007ffc`cc1c1abc e84fefc75f      call    coreclr!JIT_DbgIsJustMyCode (00007ffd`2be40a10)18 00007ffc`cc1c1ac1 90nop19 00007ffc`cc1c1ac2 8b4d18          mov     ecx,dword ptr [rbp+18h]20 00007ffc`cc1c1ac5 034d20          add     ecx,dword ptr [rbp+20h]21 00007ffc`cc1c1ac8 894dfc          mov     dword ptr [rbp-4],ecx22 00007ffc`cc1c1acb 48b9886666663ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32)23 00007ffc`cc1c1ad5 e8469ab55f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520)24 00007ffc`cc1c1ada 488945f0        mov     qword ptr [rbp-10h],rax25 00007ffc`cc1c1ade 488b4df0        mov     rcx,qword ptr [rbp-10h]26 00007ffc`cc1c1ae2 8b4518          mov     eax,dword ptr [rbp+18h]27 00007ffc`cc1c1ae5 894108          mov     dword ptr [rcx+8],eax28 00007ffc`cc1c1ae8 48b9886666663ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32)29 00007ffc`cc1c1af2 e8299ab55f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520)30 00007ffc`cc1c1af7 488945e8        mov     qword ptr [rbp-18h],rax31 00007ffc`cc1c1afb 488b4de8        mov     rcx,qword ptr [rbp-18h]32 00007ffc`cc1c1aff 8b4520          mov     eax,dword ptr [rbp+20h]33 00007ffc`cc1c1b02 894108          mov     dword ptr [rcx+8],eax34 00007ffc`cc1c1b05 48b9886666663ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32)35 00007ffc`cc1c1b0f e80c9ab55f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520)36 00007ffc`cc1c1b14 488945e0        mov     qword ptr [rbp-20h],rax37 00007ffc`cc1c1b18 4c8b4de0        mov     r9,qword ptr [rbp-20h]38 00007ffc`cc1c1b1c 8b55fc          mov     edx,dword ptr [rbp-4]39 00007ffc`cc1c1b1f 41895108        mov     dword ptr [r9+8],edx40 00007ffc`cc1c1b23 4c8b4de0        mov     r9,qword ptr [rbp-20h]41 00007ffc`cc1c1b27 488b55f0        mov     rdx,qword ptr [rbp-10h]42 00007ffc`cc1c1b2b 4c8b45e8        mov     r8,qword ptr [rbp-18h]43 00007ffc`cc1c1b2f 48b9880a8d9bf7020000 mov rcx,2F79B8D0A88h ("Adding {0}+{1}={2}")44 00007ffc`cc1c1b39 ff15d12c0d00    call    qword ptr [00007ffc`cc294810]45 00007ffc`cc1c1b3f 90nop46 00007ffc`cc1c1b40 90nop47 00007ffc`cc1c1b41 4883c440        add     rsp,40h48 00007ffc`cc1c1b45 5d              pop     rbp49 00007ffc`cc1c1b46 c3              ret

在反汇编代码的第一部分很清楚的表明方法的名称,并且是 JIT 生成的。第四行【Begin 00007FFCCC1C1A90, size b7】表示方法的起始地址和生成代码的大小。
设置断点,执行命令【 bp 00007ffccc1c1a90】。

1 0:002> bp 00007ffccc1c1a90

断点设置成功后,
但是我在运行调试的时候出错,还没有找到原因和解决办法,如果有知道原因的,不吝赐教。


2)、使用【Windbg Preview】调试
调试源码:ExampleCore_3_1_4
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_4.exe,进入到调试器。
先执行【g】命令,运行调试器。等我们的控制台输出:Press any key(1st instance function),我们在按任意键继续。

控制台程序如图:

我们按【回车键】,如下:

这时,我们回到【Windbg Preview】调试器中,调试窗口是这样的,如图:

我们点击【Break】按钮,让调试器进入中断模式。

1 (39a0.944): Break instruction exception - code 80000003(first chance)2 ntdll!DbgBreakPoint:3 00007ffd`f89ee880 cc              int     3

我们使用【
!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint
】命令,查看方法的信息。

0:001> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint
Module: 00007ffcccd0e0a0
Assembly: ExampleCore_3_1_4.dll
Token:
0000000006000003MethodDesc: 00007ffcccd39398
Name: ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
JITTED Code Address: 00007ffcccc81c40

JITTED
表示已经是编译过的,编译的地址是:
00007ffcccc81c40。当然,我们可以使用【!U
00007ffcccc81c40
】命令查看汇编代码。

1 0:001> !U 00007ffcccc81c402 Normal JIT generated code (说明是 JIT 生成的)3 ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)(方法的名称,说明我们获取的地址是对的)4 ilAddr is 00000217571620AC pImport is00000214BF4B04805 Begin 00007FFCCCC81C40, size b7(代码的开始地址和生成代码的大小)6 
7 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 24:8 >>> 00007ffc`ccc81c40 55push    rbp9 00007ffc`ccc81c41 4883ec40        sub     rsp,40h10 00007ffc`ccc81c45 488d6c2440      lea     rbp,[rsp+40h]11 00007ffc`ccc81c4a c5d857e4        vxorps  xmm4,xmm4,xmm412 00007ffc`ccc81c4e c5f97f65e0      vmovdqa xmmword ptr [rbp-20h],xmm413 00007ffc`ccc81c53 c5f97f65f0      vmovdqa xmmword ptr [rbp-10h],xmm414 00007ffc`ccc81c58 48894d10        mov     qword ptr [rbp+10h],rcx15 00007ffc`ccc81c5c 895518          mov     dword ptr [rbp+18h],edx16 00007ffc`ccc81c5f 44894520        mov     dword ptr [rbp+20h],r8d17 00007ffc`ccc81c63 833dbec6080000  cmp     dword ptr [00007ffc`ccd0e328],0
18 00007ffc`ccc81c6a 7405je      00007ffc`ccc81c7119 00007ffc`ccc81c6c e89fedc95f      call    coreclr!JIT_DbgIsJustMyCode (00007ffd`2c920a10)20 00007ffc`ccc81c71 90nop21 
22 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 25:23 00007ffc`ccc81c72 8b4d18          mov     ecx,dword ptr [rbp+18h]24 00007ffc`ccc81c75 034d20          add     ecx,dword ptr [rbp+20h]25 00007ffc`ccc81c78 894dfc          mov     dword ptr [rbp-4],ecx26 
27 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 26:28 00007ffc`ccc81c7b 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32)29 00007ffc`ccc81c85 e89698b75f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520)30 00007ffc`ccc81c8a 488945f0        mov     qword ptr [rbp-10h],rax31 00007ffc`ccc81c8e 488b4df0        mov     rcx,qword ptr [rbp-10h]32 00007ffc`ccc81c92 8b4518          mov     eax,dword ptr [rbp+18h]33 00007ffc`ccc81c95 894108          mov     dword ptr [rcx+8],eax34 00007ffc`ccc81c98 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32)35 00007ffc`ccc81ca2 e87998b75f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520)36 00007ffc`ccc81ca7 488945e8        mov     qword ptr [rbp-18h],rax37 00007ffc`ccc81cab 488b4de8        mov     rcx,qword ptr [rbp-18h]38 00007ffc`ccc81caf 8b4520          mov     eax,dword ptr [rbp+20h]39 00007ffc`ccc81cb2 894108          mov     dword ptr [rcx+8],eax40 00007ffc`ccc81cb5 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32)41 00007ffc`ccc81cbf e85c98b75f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520)42 00007ffc`ccc81cc4 488945e0        mov     qword ptr [rbp-20h],rax43 00007ffc`ccc81cc8 4c8b4de0        mov     r9,qword ptr [rbp-20h]44 00007ffc`ccc81ccc 8b55fc          mov     edx,dword ptr [rbp-4]45 00007ffc`ccc81ccf 41895108        mov     dword ptr [r9+8],edx46 00007ffc`ccc81cd3 4c8b4de0        mov     r9,qword ptr [rbp-20h]47 00007ffc`ccc81cd7 488b55f0        mov     rdx,qword ptr [rbp-10h]48 00007ffc`ccc81cdb 4c8b45e8        mov     r8,qword ptr [rbp-18h]49 00007ffc`ccc81cdf 48b9880a82ed57020000 mov rcx,257ED820A88h ("Adding {0}+{1}={2}")50 00007ffc`ccc81ce9 ff15212b0d00    call    qword ptr [00007ffc`ccd54810]51 00007ffc`ccc81cef 90nop52 
53 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 27:54 00007ffc`ccc81cf0 90nop55 00007ffc`ccc81cf1 4883c440        add     rsp,40h56 00007ffc`ccc81cf5 5d              pop     rbp57 00007ffc`ccc81cf6 c3              ret

这里比【NTSD】好看的多,不多说了。
我们使用【
bp 00007ffcccc81c40
】命令,设置断点。

1 0:001>bp 00007ffcccc81c402 
3 0:001>g4 Breakpoint 0hit5 ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint:6 00007ffc`ccc81c40 55              push    rbp

我们进入 AddAndPrint 方法的断点出了,我们就可以使用【p】或者【t】命令就行调试了。

3.6.3、在还没有被 JIT 编译的托管函数上下断点
A、知识介绍
非托管方法设置断点很容易,因为代码都已经被编译了,代码的地址就是已知的。但是,托管代码要进行两次编译才能运行。我们想要给代码设置断点,必须先找到代码的位置。这一节我们讨论在未编译的函数上如何设置断点,我们就不能使用【bp】命令,需要使用另外一个命令【bpmd】,它能自动找出被 JIT 编译后代码正确地址,并且,可以仅根据完整的方法名来设置断点。
【bpmd】命令可以用来在还没有被 JIT编译的代码上设置断点,它设置的是一个延迟断点,设置断点时位置是未知的,只有在将来某个事件发生时,才会真正的设置断点。【bpmd】命令是通过注册内部的 CLR JIT 编译通知来实现延迟断点的。当调试器收到 JIT 编译通知时,它会检查这个通知是否和现有的某一个延迟断点相关,如果相关,那么就会在函数执行之前就会使断点生效。而且,【bpmd】命令还会接受模块加载通知,这就意味着在设置断点时甚至可以不需要加载程序集。当程序集加载的时候,这个命令会再次得到通知,并检查是否有某个延迟断点位于这个模块中,如果有,便会激活这个断点。
B、眼见为实
1)、使用【NTSD】调试
调试源码:ExampleCore_3_1_4
调试任务:在 AddAndPrint 方法第一次执行前设置断点。
执行命令【D:\Program Files\Microsoft Visual Studio\2022\Community>ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\bin\Debug\net8.0\ExampleCore_3_1_4.exe】打开调试器窗口。
我们直接【g】运行调试器,看到调试器中输出:Press any key(1st instance function)

我们使用【ctrl+c】进入调试器中断模式。我们使用【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令,查看方法是否已经编译。

1 0:009> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint2 Module:      00007ffccdfde0a03 Assembly:    ExampleCore_3_1_4.dll4 Token:       0000000006000003
5 MethodDesc:  00007ffcce0093986 Name:        ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)7 Not JITTED yet. Use !bpmd -md 00007FFCCE009398 to break on run.

我们使用【!bpmd -md 00007FFCCE009398】命令设置断点。

1 0:009> !bpmd -md 00007FFCCE0093982 MethodDesc =00007FFCCE0093983 Adding pending breakpoints...

我们断点设置成功。
但是我在运行调试的时候出错,还没有找到原因和解决办法,如果有知道原因的,不吝赐教。

1 0:008>g2 g(3308.f8): CLR notification exception -code e0444143 (first chance)3 JITTED ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)4 Setting breakpoint: bp 00007FFCCDF51A90 [ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)]5 Unable to insert breakpoint 0 at 00007ffc`cdf51a90, Win32 error 0n9986     "内存位置访问无效。"
7 The breakpoint was set with BP.  If you want breakpoints8 to track module load/unload state you must use BU.9 bp0 at 00007ffc`cdf51a90 failed10 WaitForEvent failed, Win32 error 0n99811 内存位置访问无效。12 KERNELBASE!RaiseException+0x69:13 00007ffd`f5fb3e49 0f1f440000      nop     dword ptr [rax+rax]                    


2)、使用【Windbg Preview】调试
调试源码:ExampleCore_3_1_4
调试任务:在 AddAndPrint 方法第一次执行前设置断点。
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_4.exe,进入调试器。
我们使用【g】命令,继续运行,我们的控制台程序输出:Press any key(1st instance function),这是第一次输出,AddAndPrint 方法还没有执行,也就还没有编译。

我们点击【Break】按钮,中断执行,我们先证明 AddAndPrint 这个方法还没有编译,执行【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令。

1 0:001> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint2 Module:      00007ffccdfce0a03 Assembly:    ExampleCore_3_1_4.dll4 Token:       0000000006000003
5 MethodDesc:  00007ffccdff9398(这个就是方法描述符)6 Name:        ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)7 Not JITTED yet. Use !bpmd -md 00007FFCCDFF9398 to break on run.

Not JITTED yet:表示未编译。【Use !bpmd -md 00007FFCCDFF9398 to
break on run.
】这句话是说可以通过使用【bpmd】命令和方法描述符来设置一个断点。

1 0:001> !bpmd -md 00007ffccdff93982 MethodDesc =00007FFCCDFF93983 Adding pending breakpoints...

当我们使用【g】命令继续执行,并在控制台应用程序中按下【回车键】,调试器输出如下:

1 0:001>g2 (21e8.31c8): CLR notification exception - code e0444143 (first chance)3 JITTED ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)4 Setting breakpoint: bp 00007FFCCDF41C40 [ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)]5 Breakpoint 0hit6 ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint:7 00007ffc`cdf41c40 55              push    rbp

需要注意的是 notification 部分的输出,调试器已经接受到了 CLR 通知异常(
e0444143
),JIT 编译方法的时候,重新设置了断点在地址:
00007FFCCDF41C40
,并且成功在断点处中断执行。


3.6.4、在预编译的程序集中设置断点
.NET 代码也需要在进程的上下文中执行。JIT 编译器将程序集的 IL 代码编译为机器代码,每当 .NET 代码访问同一段代码时,CLR 首先检查它是否已经被编译了,如果是,则重用已编译的代码。当然,当进程结束了,JIT 编译器生成的所有机器代码也会随之消失。当下一次需要执行程序集时,JIT 编译器再重新对相同的代码进行编译。
预编译程序集是与某个程序集对应的非托管映像,其中全部的代码已经全部被编译为机器代码。如果 CLR 需要执行这个程序集中的代码,并且这个程序集在机器上有一个非托管的映像,就会直接跳过 JIT 编译步骤,并直接从这个非托管映像中加载机器代码。
需要说明一点,这本书写的有点早,那个时候只有 .NET Framework 平台,NGEN 也是针对 .NET Framework 平台的。我这个系列是针对 .NET 8,也就是跨平台的版本,所以是不能直接使用 NGEN 生成预编译的程序集的。如果想生成跨平台的预编译程序集,需要使用  CrossGen

NET 6 引入了 CrossGen2,它是已被删除的 CrossGen 的后继版本。 CrossGen 和 CrossGen2 是用于提供预先 (AOT) 编译的工具,可改进应用的启动时间。 CrossGen2 是用 C# (而不是 C++)编写的,可执行之前的版本无法实现的分析和优化。 如果想了解 CrossGen2,可以去微软官网:
https://devblogs.microsoft.com/dotnet/conversation-about-crossgen2/

3.6.5、在泛型方法上设置断点
A、知识介绍
如果我们想对泛型类型的方法下断点,最首要的任务就是找到泛型类型的名称和方法的名称,找到之后,我们就可以下断点了。找泛型类型的名称和方法的名称有两种办法,第一种是通过命令,第二种是我们可以使用 ILSpy 找到。

B、眼见为实
我们想要在泛型类型的方法上下断点,首要的任务是找到泛型类型的名称和方法的名称,这是关键。
1)、使用【NTSD】调试
a、我们通过 Windbg 和 SOS 的命令找到类型的名称。
编译好我们的项目,打开【Visual Studio 2022 Developer Command Prompt】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.exe】,打开调试器。
使用【g】命令,运行调试器。

1 0:000>g2 ModLoad: 00007ffe`90a90000 00007ffe`90ac0000   C:\Windows\System32\IMM32.DLL3 ModLoad: 00007ffe`4f1d0000 00007ffe`4f229000   C:\Program Files\dotnet\host\fxr\8.0.2\hostfxr.dll4 ModLoad: 00007ffe`3b840000 00007ffe`3b8a4000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\hostpolicy.dll5 ModLoad: 00007ffe`28ab0000 00007ffe`28f98000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\coreclr.dll6 ModLoad: 00007ffe`914d0000 00007ffe`915f9000   C:\Windows\System32\ole32.dll7 ModLoad: 00007ffe`91f30000 00007ffe`92284000C:\Windows\System32\combase.dll8 ModLoad: 00007ffe`918a0000 00007ffe`91975000C:\Windows\System32\OLEAUT32.dll9 ModLoad: 00007ffe`902c0000 00007ffe`9033f000   C:\Windows\System32\bcryptPrimitives.dll10 (3b74.3fac): Unknown exception - code 04242420(first chance)11 ModLoad: 00007ffe`27be0000 00007ffe`2886c000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll12 ModLoad: 00007ffe`27a20000 00007ffe`27bd9000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll13 ModLoad: 00007ffe`8f900000 00007ffe`8f913000   C:\Windows\System32\kernel.appcore.dll14 ModLoad: 000001ba`47fc0000 000001ba`47fc8000   E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll15 ModLoad: 000001ba`47fd0000 000001ba`47fde000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll16 ModLoad: 00007ffe`75c60000 00007ffe`75c88000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll17 (3b74.3fac): Break instruction exception - code 80000003 (first chance)

按【ctrl+c】组合键进入中断模式。
输入【!dumpdomain】命令查看应用程序域详情,该命令会列出每个应用程序域中加载的所有程序集和模块。

1 0:000> !dumpdomain2 --------------------------------------
3 System Domain:      00007ffe28f460d04 LowFrequencyHeap:   00007FFE28F465A85 HighFrequencyHeap:  00007FFE28F466386 StubHeap:           00007FFE28F466C87 Stage:              OPEN8 Name:               None9 --------------------------------------
10 Domain 1:           000001ba465a1ff011 LowFrequencyHeap:   00007FFE28F465A812 HighFrequencyHeap:  00007FFE28F4663813 StubHeap:           00007FFE28F466C814 Stage:              OPEN15 Name:               clrhost16 Assembly:           000001ba46564d20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll]17 ClassLoader:        000001BA46564DB018 Module19   00007ffdc8f44000    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll20 
21 Assembly:           000001ba465506e0 [E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll]22 ClassLoader:        000001BA46550FC023 Module24   00007ffdc912e0a0    E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll25 
26 Assembly:           000001ba465507e0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll]27 ClassLoader:        000001BA4655087028 Module29   00007ffdc912fbc8    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll30 
31 Assembly:           000001ba47f97460 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll]32 ClassLoader:        000001BA47F97DE033 Module34   00007ffdc91597f0    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll

我们找到了模块,就可以将模块中所有的类型输出来,可以使用【!dumpmodule -mt 】命令。

1 0:000> !dumpmodule -mt 00007ffdc912e0a02 Name: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll3 Attributes:              PEFile4 TransientFlags:          00209011
5 Assembly:                000001ba465506e06 BaseAddress:             000001BA47FC00007 PEFile:                  000001BA4654FCD08 ModuleId:                00007FFDC912E4589 ModuleIndex:             0000000000000001
10 LoaderHeap:              00007FFE28F4659811 TypeDefToMethodTableMap: 00007FFDC913432012 TypeRefToMethodTableMap: 00007FFDC913434013 MethodDefToDescMap:      00007FFDC913447014 FieldDefToDescMap:       00007FFDC913449815 MemberRefToDescMap:      00007FFDC91343D016 FileReferencesMap:       0000000000000000
17 AssemblyReferencesMap:   00007FFDC91344B818 MetaData start address:  000001BA47FC20A8 (1612bytes)19 
20 Types defined in thismodule21 
22 MT          TypeDef Name23 ------------------------------------------------------------------------------
24 00007ffdc91500e8 0x02000002ExampleCore_3_1_5.Program25 00007ffdc91593e0 0x02000003 ExampleCore_3_1_5.MyList`1
26 
27 Types referenced in thismodule28 
29 MT            TypeRef Name30 ------------------------------------------------------------------------------
31 00007ffdc8fd5fa8 0x0200000dSystem.Object32 00007ffdc9159700 0x02000010System.Diagnostics.Debugger33 00007ffdc915ab08 0x02000011 System.Console

红色标注的就是我们要查找泛型类型真实的名称。有了类型,我们继续可以使用【!dumpmt -md 00007ffdc91593e0】命令,输出它所有方法。

1 0:000> !dumpmt -md 00007ffdc91593e02 EEClass:         00007FFDC9161F483 Module:          00007FFDC912E0A04 Name:            ExampleCore_3_1_5.MyList`1
5 mdToken:         0000000002000003
6 File:            E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll7 BaseSize:        0x18
8 ComponentSize:   0x0
9 DynamicStatics:  false
10 ContainsPointers true
11 Slots in VTable: 6
12 Number of IFaces in IFaceMap: 0
13 --------------------------------------
14 MethodDesc Table15 Entry       MethodDesc    JIT Name16 00007FFDC8FE0048 00007FFDC8FD5F38   NONE System.Object.Finalize()17 00007FFDC8FE0060 00007FFDC8FD5F48   NONE System.Object.ToString()18 00007FFDC8FE0078 00007FFDC8FD5F58   NONE System.Object.Equals(System.Object)19 00007FFDC8FE00C0 00007FFDC8FD5F98   NONE System.Object.GetHashCode()20 00007FFDC914B948 00007FFDC91593B8   NONE ExampleCore_3_1_5.MyList`1..ctor()21 00007FFDC914B930 00007FFDC91593A8   NONE ExampleCore_3_1_5.MyList`1.Add(!0)

红色标记就是我们要查找的 Add 方法,有了方法的地址,我们就可以使用【!bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add】命令为其下断点了。

1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add2 MethodDesc =00007FFDC91593A83 Adding pending breakpoints...

断点设置成功。
但是我在运行调试的时候出错,还没有找到原因和解决办法,如果有知道原因的,不吝赐教。

1 0:000>g2 (3b74.3fac): CLR notification exception -code e0444143 (first chance)3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)4 Setting breakpoint: bp 00007FFDC90A1A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)]5 Unable to insert breakpoint 0at 00007ffd`c90a1a50, Win32 error 0n9986     "内存位置访问无效。"
7 The breakpoint was setwith BP.  If you want breakpoints8 to track module load/unload state you must use BU.9 bp0 at 00007ffd`c90a1a50 failed10 WaitForEvent failed, Win32 error 0n99811 内存位置访问无效。12 KERNELBASE!RaiseException+0x69:13 00007ffe`8fb03e49 0f1f440000      nop     dword ptr [rax+rax]


b、我们可以使用 ILSpy 或者 SnPay 来查找泛型类型的名称和方法的名称。
和使用【Windbg Preview】这节的内容一样。

2)、使用【Windbg Preview】调试
调试源码:ExampleCore_3_1_5
a、我们通过 Windbg 和 SOS 的命令找到类型的名称。
编译程序集后,泛型类型一定在这个程序集的模块中。然后我们再在这个模块中打印出所有的类型,就可以找到这个类型了。
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_5.exe。进入到调试器后,我们使用【g】命令运行调试器,调试器会在 Program 类型的 Main 方法的【Debugger.Break()】这个行代码中断执行。我们点击【break】按钮,进入调试模式。
我们现在这个程序集中查找模块信息,我们可以使用【!dumpdomain】命令。

1 0:000> !dumpdomain2 --------------------------------------
3 System Domain:      00007ffe226360d04 LowFrequencyHeap:   00007FFE226365A85 HighFrequencyHeap:  00007FFE226366386 StubHeap:           00007FFE226366C87 Stage:              OPEN8 Name:               None9 --------------------------------------
10 Domain 1:           000001a1469ddc4011 LowFrequencyHeap:   00007FFE226365A812 HighFrequencyHeap:  00007FFE2263663813 StubHeap:           00007FFE226366C814 Stage:              OPEN15 Name:               clrhost16 Assembly:           000001a146a2e010 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll]17 ClassLoader:        000001A146A2E0A018 Module19   00007ffdc2634000    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll20 
21 Assembly:           000001a1484c2bf0 [E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll]22 ClassLoader:        000001A1484C2C8023 Module24   00007ffdc281e0a0    E:\Visual Studio 2022\Source\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll25 
26 Assembly:           000001a146976520 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll]27 ClassLoader:        000001A1469765B028 Module29   00007ffdc281fbc8    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll

00007ffdc281e0a0
这个地址就是我们程序集(ExampleCore_3_1_5.dll)的模块地址。我们找到了模块,就可以将模块中所有的类型输出来,可以使用【!dumpmodule -mt 】命令。

1 0:000> !dumpmodule -mt 00007ffdc281e0a02 Name: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll3 Attributes:              PEFile4 TransientFlags:          00209011 
5 Assembly:                000001a1484c2bf06 BaseAddress:             000001A1468F00007 PEAssembly:              000001A14696F6A08 ModuleId:                00007FFDC281E4589 ModuleIndex:             0000000000000001
10 LoaderHeap:              00007FFE2263659811 TypeDefToMethodTableMap: 00007FFDC282432012 TypeRefToMethodTableMap: 00007FFDC282434013 MethodDefToDescMap:      00007FFDC282447014 FieldDefToDescMap:       00007FFDC282449815 MemberRefToDescMap:      00007FFDC28243D016 FileReferencesMap:       0000000000000000
17 AssemblyReferencesMap:   00007FFDC28244B818 MetaData start address:  000001A1468F20A8 (1612bytes)19 
20 Types defined in thismodule21 
22 MT          TypeDef Name23 ------------------------------------------------------------------------------
24 00007ffdc28400e8 0x02000002ExampleCore_3_1_5.Program25 00007ffdc28493e0 0x02000003 ExampleCore_3_1_5.MyList`1
26 
27 Types referenced in thismodule28 
29 MT            TypeRef Name30 ------------------------------------------------------------------------------
31 00007ffdc26c5fa8 0x0200000dSystem.Object32 00007ffdc2849700 0x02000010System.Diagnostics.Debugger33 00007ffdc284ab08 0x02000011 System.Console

ExampleCore_3_1_5.MyList`
1
就是泛型类型编译后的名称。红色标注的就是我们要查找泛型类型真实的名称。有了类型,我们继续可以使用【!dumpmt -md
00007ffdc28493e0
】命令,输出它所有方法。

1 0:000> !dumpmt -md 00007ffdc28493e02 EEClass:             00007ffdc2851f483 Module:              00007ffdc281e0a04 Name:                ExampleCore_3_1_5.MyList`1
5 mdToken:             0000000002000003
6 File:                E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
8 BaseSize:            0x18
9 ComponentSize:       0x0
10 DynamicStatics:      false
11 ContainsPointers:    true
12 Slots in VTable:     6
13 Number of IFaces in IFaceMap: 0
14 --------------------------------------
15 MethodDesc Table16 Entry       MethodDesc    JIT Name17 00007FFDC26D0048 00007ffdc26c5f38   NONE System.Object.Finalize()18 00007FFDC26D0060 00007ffdc26c5f48   NONE System.Object.ToString()19 00007FFDC26D0078 00007ffdc26c5f58   NONE System.Object.Equals(System.Object)20 00007FFDC26D00C0 00007ffdc26c5f98   NONE System.Object.GetHashCode()21 00007FFDC283B948 00007ffdc28493b8   NONE ExampleCore_3_1_5.MyList`1..ctor()22 00007FFDC283B930 00007ffdc28493a8   NONE ExampleCore_3_1_5.MyList`1.Add(!0)

红色标记就是我们要查找的 Add 方法,有了方法的地址,我们就可以使用【bpmd】命令为其下断点了。

1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add2 MethodDesc =00007FFDC28493A83 Adding pending breakpoints...

断点设置成功后,我们使用【g】命令,程序继续运行,就可以在断点处暂停。

1 0:000>g2 (2c88.2734): CLR notification exception -code e0444143 (first chance)3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)4 Setting breakpoint: bp 00007FFDC2791A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)]5 Breakpoint 0hit6 ExampleCore_3_1_5!ExampleCore_3_1_5.MyList<int>.Add:7 00007ffd`c2791a50 55              push    rbp

断点效果如图:

b、我们可以使用 ILSpy 或者 SnPay 来查找泛型类型的名称和方法的名称。
我们可以使用 ILSpy 或者 snSpy 查看泛型类型和方法的名称。我们打开【ILSpy】工具,加载我们的 ExampleCore_3_1_5.dll 文件。再左侧,依次点击【Metadata】--->【Tables】--->【TypeDef】,在右侧就能看到这个程序集中定义的所有的类型名称。如图:

我们知道了类型的名称,然后就是查找方法的名称。也很简单。

现在我们知道了泛型类型的名称和方法的名称,就可以直接设置断点了。
编译好我们的项目,打开【Windbg Preview】,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_3_1_5.exe。进入到调试器后,我们使用【g】命令运行调试器,调试器会在 Program 类型的 Main 方法的【Debugger.Break()】这个行代码中断执行。我们点击【break】按钮,进入调试模式。
我们执行命令【!bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`
1
.Add

】,就可以直接下断点了。

1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add2 MethodDesc =00007FFDC2DE93A83 Adding pending breakpoints...

断点设置成功后,我们使用【g】命令,程序继续运行,就可以在断点处暂停。

1 0:000>g2 (10b4.42c4): CLR notification exception -code e0444143 (first chance)3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)4 Setting breakpoint: bp 00007FFDC2D31A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)]5 Breakpoint 0hit6 ExampleCore_3_1_5!ExampleCore_3_1_5.MyList<int>.Add:7 00007ffd`c2d31a50 55              push    rbp

断点设置成功,我们也完成我们的任务。

四、总结
这篇文章终于写完了,是这篇文章的“上”篇写完了,“下”篇还没有开始呢,这篇文章写作周期也不短,内容实在多。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

热点随笔:

·
推荐10款C#开源好用的Windows软件
(
追逐时光者
)
·
都说了能不动就别动,非要去调整,出生产事故了吧 → 补充
(
青石路
)
·
【信息化】软考高项(信息系统项目管理师)论文自我总结精华和模板
(
Roushan_IT
)
·
遭遇DDOS攻击忍气吞声?立刻报警!首都网警重拳出击,犯罪分子无所遁形
(
刘悦的技术博客
)
·
可用于智能客服的完全开源免费商用的知识库项目
(
tokengo
)
·
《HelloGitHub》第 95 期
(
削微寒
)
·
细聊ASP.NET Core WebAPI格式化程序
(
yi念之间
)
·
新来个架构师,用48张图把OpenFeign原理讲的炉火纯青~~
(
三友的java日记
)
·
万字长文学会对接 AI 模型:Semantic Kernel 和 Kernel Memory,工良出品,超简单的教程
(
痴者工良
)
·
【八股总结】至今为止遇到的八股(上半)
(
dayceng
)
·
10个技巧,3分钟教会你高效寻找开源项目
(
知微之见
)
·
都说了别用BeanUtils.copyProperties,这不翻车了吧
(
程序员老猫
)

热点新闻:

·
阿里云简单粗暴大降价!或将引发行业连锁反应
·
月球!我们要从海南出发了
·
十年长跑后,苹果宣布逐步缩减造车计划,转向生成式AI
·
炮轰Sora,杨立昆为什么不看好生成式AI|中企荐读
·
马斯克起诉OpenAI及其CEO奥特曼,要求恢复开源
·
不听劝的小米,又被比亚迪上了一课
·
不认2月29?歇菜的可远不止自动驾驶
·
哪吒员工吐槽年终奖迟发,CEO回应:部分员工不习惯过苦日子,要将寒气传给每个人
·
海底光缆断裂意味着什么
·
互联网大厂,花名不能停
·
超级光盘中国造!一张就能装下小型数据中心
·
极小质量物体的引力成功测得 有助进一步探索量子引力理论

本文介绍基于
Python

ArcPy
模块,实现基于
栅格图像
批量裁剪
栅格图像
,同时
对齐
各个栅格图像的
空间范围

统一
其各自
行数

列数
的方法。

首先明确一下我们的需求。现有某一地区的多张栅格遥感影像,其虽然都大致对应着同样的地物范围,但不同栅格影像之间的
空间范围

行数

列数
、像元的
位置
等都不完全一致;例如,某一景栅格影像会比其他栅格影像多出一行,而另一景栅格影像可能又会比其他栅格影像少一列等等。我们希望可以以其中某一景栅格影像为标准,将全部的栅格影像的具体范围、行数、列数等加以统一。

本文所用到的具体代码如下。

# -*- coding: utf-8 -*-
"""
Created on Thu Dec 29 21:13:19 2022

@author: fkxxgis
"""

import arcpy

tif_file_path = r"E:\02_Project\01_Chlorophyll\ClimateZone\Original"
result_file_path = r"E:\02_Project\01_Chlorophyll\ClimateZone\Original_Snap/"
snap_file_name = r"E:\02_Project\01_Chlorophyll\ClimateZone\Original\F_LC.tif"

arcpy.env.workspace = tif_file_path
arcpy.env.snapRaster = snap_file_name

tif_file_list = arcpy.ListRasters("*", "tif")

for tif_file in tif_file_list:
    key_name = tif_file.split(".tif")[0] + "S.tif"
    arcpy.Clip_management(tif_file,
                          "#",
                          result_file_path + key_name,
                          snap_file_name,
                          "#",
                          "#",
                          "MAINTAIN_EXTENT")

其中,
tif_file_path
是保存有我们原有栅格图像的路径,
result_file_path
是裁剪后各个结果图像的保存路径(记得在这一路径后加一个正斜杠
/
,否则之后输出结果的路径会有问题),
snap_file_name
是裁剪其他栅格图像时,所用的模板栅格图像——因为我们要统一各个栅格图像的行号与列号,所以很显然,这里这个模板图像就需要找各个栅格图像中,
行数与列数均为最少的那一景图像
。这里需要注意,如果大家的各个栅格图像中,行数与列数最少的栅格
不是同一个栅格
,那么可以分别用行数最少、列数最少的这两个栅格分别作为模板,执行两次上述代码。

代码整体思路也很简单:首先,我们基于
arcpy.ListRasters()
函数,获取
tif_file_path
路径下原有的全部
.tif
格式的图像文件,并以列表的形式存放于
tif_file_list
中;随后,逐一取出
tif_file_list
列表中的栅格文件,进行裁剪处理。这里的裁剪我们是通过
arcpy.Clip_management()
函数来实现的,其各项参数的具体含义大家可以参考官方帮助文档,我们这里就只对本文中需要修改的参数加以介绍。

其中,第一个参数就是当前循环所用的栅格图像文件,第三个参数是结果文件的保存路径与文件名,第四个参数则是模板文件;最后一个参数
"MAINTAIN_EXTENT"
是为了保证得到的裁剪后
结果图像
严格与
模板图像
的行数、列数相匹配。除此之外,几个
"#"
表示我们对其他参数暂时不配置。

此外,在代码开头的这句
arcpy.env.snapRaster = snap_file_name
,表明我们将以所选用的模板文件为标准,使得输出的结果文件的像元大小、图像范围等与模板文件保持一致。这里需要注意,这一句代码与前述的
"MAINTAIN_EXTENT"
参数缺一不可——只有二者同时出现,才可以保证输出结果与模板文件是严格一致的。

另一方面,由于我们用到了
ArcPy
模块,因此如果大家的
Python
版本是
3.0
及以上,则需要在
ArcMap
软件中的
Python运行框
,或其对应的
IDLE
(如下图所示)中运行上述代码。

image

运行结果后,可以发现所有输出结果文件就具有完全一致的行数与列数了,且其各自的像元位置也是完全一致的。

至此,大功告成。