Windbg命令脚本流程控制语句详解

时间:2023-03-27 22:19:46
Windbg命令脚本流程控制语句详解

Windbg命令脚本一文里,我们介绍了命令脚本语言的的组成要素,在本文里将对语句进行展开的讲解。这些语句主要是流程控制的语句,比如我们常见的条件分子和循环语句等。

; (命令分隔符)

分号(;)字符用于在一行中分隔多个命令。

Command1 ; Command2 [; Command3 ...]

参数

命令1,命令2,…

要执行的命令。

命令从左到右依次执行。除非另有规定,否则单行上的所有命令都引用当前线程。如果命令导致线程执行,则该行上的其余命令将被延迟,直到该线程在调试事件上停止。少数命令后面不能跟分号,因为它们会自动将行的整个剩余部分作为参数。其中包括as(set alias)、$<(run script file)、$><(run script file)和以*(comment line specifier)标记开头的任何命令。
下面是一个例子。这将执行当前程序到源代码行123,打印计数器的值,然后继续执行:
0:000> g `:123`; ? poi(counter); g

{ } (块分割符)

 一对大括号({})用于包围调试器命令程序中的语句块
Statements { Statements } Statements

输入每个块后,将计算块中的所有别名。如果在命令块中的某个点更改别名的值,则该点后面的命令将不会使用新的别名值,除非它们位于从属块中。每个块必须以控制流标记开头。如果您希望创建一个仅用于评估别名的块,则应在其前面加上.block标记。

${ } (别名解释器)

后面跟一对大括号(${})的美元符号计算出与指定的用户别名相关的各种值。

Text ${Alias} Text
Text ${/d:Alias} Text
Text ${/f:Alias} Text
Text ${/n:Alias} Text
Text ${/v:Alias} Text

参数

Alias

指定要展开或计算的别名的名称。 别名必须是用户命名别名或变量使用值.foreach语句

/d

计算结果为一个或零个具体情况取决于是否当前定义别名。 如果定义别名,则 ${/ v 部分:别名} 替换为 1; 如果未定义别名, ${/ v 部分:别名} 替换为 0。

/f
 如果当前定义了别名,则计算为等效别名。如果定义了别名,${/f:alias}将替换为别名等效项;如果未定义别名,${/f:alias}将替换为空字符串。
/n

如果当前定义了别名,则计算为别名名称。如果定义了别名,${/n:alias}将被别名替换;如果未定义别名,${/n:alias}将不被替换,但保留其文字值${/n:alias}。

/v

阻止任何别名计算。无论是否定义了别名,${/v:alias}始终保留其文本值${/v:alias}。

如果没有使用任何开关,并且当前定义了别名,${Alias}将替换为别名等效项。如果没有使用任何开关且未定义别名,${Alias}始终保留其文字值${Alias}。使用${}标记的一个优点是,即使别名与其他字符相邻,也将对其进行评估。如果没有这个标记,调试器只替换别名,这些别名与其他标记之间用空格分隔。如前所述,在某些情况下,标记不会被任何内容替换,而是保留其文字值。当不使用开关且别名未定义时、使用/n开关且别名未定义时以及使用/v开关时,都会发生这种情况。在这种情况下,语句保留其面值,包括$符号和大括号。因此,如果将其用作命令的参数,将导致语法错误,除非该参数接受任意文本字符串。然而,有一个例外。如果使用${/v:alias}作为as(set alias)或as(set alias)命令的第一个参数,则此令牌将单独被视为字符串别名,而不是字符串${/v:alias}。这只适用于as、as和ad命令,并且仅在使用/v开关时才有效。当它们保留其文本值时,它将不适用于${/n:alias}或${alias}。别名必须是名为alias的用户或.foreach标记使用的变量值,而不是固定名称别名。如果字符串别名中存在固定名称别名,则在评估${}标记之前将替换该别名。

$$ (注释说明符)

 如果命令开头出现两个美元符号($$),则该行的其余部分将被视为注释,除非注释以分号结尾。
$$ [any text]
与任何其他调试器命令一样,对$$标记进行分析。因此,如果要在另一个命令之后创建注释,则必须在$$标记前面加上分号。$$标记将导致忽略后面的文本,直到行尾或遇到分号为止。分号终止注释;分号后的文本被解析为标准命令。这与*(注释行说明符)不同,后者使行的其余部分成为注释,即使存在分号。
例如,以下命令将显示EAX和EBX,但不显示ECX:
0:000> r eax; $$ some text; r ebx; * more text; r ecx

不以任何方式处理前缀为*或$$标记的文本。如果正在执行远程调试,则在调试服务器中输入的注释在调试客户端中将不可见,反之亦然。如果希望使注释文本以所有参与方都可见的方式显示在调试器命令窗口中,则应使用.echo(echo comment)。

* (注释行说明符)

如果星号(*)字符位于命令的开头,则行的其余部分将被视为注释,即使在其后面出现分号。
* [any text]

*标记的解析方式与任何其他调试器命令相同。因此,如果要在另一个命令之后创建注释,则必须在*标记前面加上分号。*标记将导致忽略行的其余部分,即使行后面出现分号。这与$$(注释说明符)不同,后者创建的注释可以用分号终止。

例如,以下命令将显示EAX和EBX,但不显示ECX:
0:000> r eax; $$ some text; r ebx; * more text; r ecx

不以任何方式处理前缀为*或$$标记的文本。如果正在执行远程调试,则在调试服务器中输入的注释在调试客户端中将不可见,反之亦然。如果希望使注释文本以所有参与方都可见的方式显示在调试器命令窗口中,则应使用.echo(echo comment)。

.block

.block标记不执行任何操作;它仅用于引入语句块。
Commands ; .block { Commands } ; Commands

命令块由大括号包围。输入每个块后,将计算块中的所有别名。如果在命令块中的某个点更改别名的值,则该点后面的命令将不会使用新的别名值,除非它们位于从属块中。每个块必须以控制流标记开头。如果您希望创建一个仅用于评估别名的块,则应在其前面加上.block标记,因为此标记除了允许引入块之外没有其他作用。

.break

.break标记的行为类似于c中的break关键字。
.for (...) { ... ; .if (Condition) .break ; ...} 

.while (...) { ... ; .if (Condition) .break ; ...} 

.do { ... ; .if (Condition) .break ; ...} (...)

.break标记可以在任何.for、.while或.do循环中使用。由于没有与c goto语句等效的控制流标记,因此通常在.if条件中使用.break标记,如上面的语法示例所示。然而,这实际上不是必需的。

.catch

.catch标记用于防止程序在发生错误时终止。它不像C++中的catch关键字。
Commands ; .catch { Commands } ; Commands

.catch标记后面跟着大括号,其中包含一个或多个命令。如果.catch块中的命令生成错误,将显示错误消息,大括号内的所有剩余命令将被忽略,并在右大括号后使用第一个命令继续执行。如果不使用.catch,则错误将终止整个调试器命令程序。可以使用.leave退出.catch块。

.leave

 .leave标记用于退出.catch块。
.catch { ... ; .if (Condition) .leave ; ... }

当在.catch块中遇到.leave标记时,程序退出该块,并在右大括号后使用第一个命令继续执行。

.continue

.continue标记的行为类似于c中的continue关键字。
.for (...) { ... ; .if (Condition) .continue ; ... } 

.while (...) { ... ; .if (Condition) .continue ; ... } 

.do { ... ; .if (Condition) .continue ; ... } (...)

.Continue标记可以在任何.for、.while或.do循环中使用。

.do

.do标记的行为类似于c中的do关键字,但条件之前不使用单词“while”。

.do { Commands } (Condition)

语法元素:

  • 命令
    指定一个或多个将在条件为真时重复执行的命令,但始终至少执行一次。这个命令块需要用大括号括起来,即使它由一个命令组成。多个命令应该用分号分隔,但右大括号前的最后一个命令不需要后跟分号。
  • 条件
    指定条件。如果计算结果为零,则视为假;否则为真。括号中的封闭条件是可选的。条件必须是表达式,而不是调试器命令。它将由默认表达式求值器(MASM或C++)进行计算。
.break和.continue标记可用于退出或重新启动命令块。

.else

.else标记的行为与c中的else关键字类似。
.if (Condition) { Commands } .else { Commands } 

.if (Condition) { Commands } .elsif (Condition) { Commands } .else { Commands }
语法要素:
  • 命令
    指定将按条件执行的一个或多个命令。这个命令块需要用大括号括起来,即使它由一个命令组成。多个命令应该用分号分隔,但右大括号前的最后一个命令不需要后跟分号。

.elsif

.elsif标记的行为与c中的else-if关键字组合类似。

.if (Condition) { Commands } .elsif (Condition) { Commands } 

.if (Condition) { Commands } .elsif (Condition) { Commands } .else { Commands }

语法要素:

  • 条件
    指定条件。如果计算结果为零,则视为假;否则为真。括号中的封闭条件是可选的。条件必须是表达式,而不是调试器命令。它将由默认表达式求值器(MASM或C++)进行评估。
  • 命令
     指定将按条件执行的一个或多个命令。这个命令块需要用大括号括起来,即使它由一个命令组成。多个命令应该用分号分隔,但右大括号前的最后一个命令不需要后跟分号。

.if

.if标记的行为类似于c中的if关键字。

.if (Condition) { Commands } 

.if (Condition) { Commands } .else { Commands } 

.if (Condition) { Commands } .elsif (Condition) { Commands } 

.if (Condition) { Commands } .elsif (Condition) { Commands } .else { Commands }

语法要素:

  • 条件
    指定条件。如果计算结果为零,则视为假;否则为真。括号中的封闭条件是可选的。条件必须是表达式,而不是调试器命令。它将由默认表达式求值器(MASM或C++)进行计算。
  • 命令
     指定将按条件执行的一个或多个命令。这个命令块需要用大括号括起来,即使它由一个命令组成。多个命令应该用分号分隔,但右大括号前的最后一个命令不需要后跟分号。

.for

.for标记的行为类似于c中的for关键字,但多个增量命令必须用分号分隔,而不是用逗号分隔。

.for (InitialCommand ; Condition ; IncrementCommands) { Commands }

语法要素:

  • 初始化命令
    指定将在循环开始之前执行的命令。只允许使用一个初始命令。
  • 条件
    指定条件。如果计算结果为零,则视为假;否则为真。括号中的封闭条件是可选的。条件必须是表达式,而不是调试器命令。它将由默认表达式求值器(MASM或C++)进行计算。
  • 递增命令
    指定将在每个循环结束时执行的一个或多个命令。如果要使用多个增量命令,请用分号分隔它们,但不要将它们括在大括号中。
  • 命令
    指定一个或多个将在条件为真时重复执行的命令。这个命令块需要用大括号括起来,即使它由一个命令组成。多个命令应该用分号分隔,但右大括号前的最后一个命令不需要后跟分号。
如果所有的工作都是由increment命令完成的,那么可以完全忽略条件,只需使用一对空的大括号。
下面是.for语句的示例,其中包含多个递增命令:
0:000> .for (r eax=0; @eax < 7; r eax=@eax+1; r ebx=@ebx+1) { .... }

.foreach

.foreach标记解析一个或多个调试器命令的输出,并将此输出中的每个值用作一个或多个附加命令的输入。

.foreach [Options] ( Variable  { InCommands } ) { OutCommands } 

.foreach [Options] /s ( Variable  "InString" ) { OutCommands } 

.foreach [Options] /f ( Variable  "InFile" ) { OutCommands }

语法要素:

  • 选项
    可以是以下选项的任意组合:
    /PS首字母kipnumber 导致跳过某些初始令牌。initialkipnumber指定不会传递给指定outcommands的输出令牌数。
    /PS跳过编号  导致每次处理命令时重复跳过令牌。每次将令牌传递给指定的outcommands后,将忽略相当于skipNumber值的一些令牌。
  • 变量
    指定变量名。此变量将用于保存incommands字符串中每个命令的输出;您可以在传递给outcommands的参数中按名称引用变量。可以使用任何字母数字字符串,但不建议使用还可以传递有效十六进制数或调试器命令的字符串。如果用于变量的名称恰好与现有全局变量、局部变量或别名匹配,则它们的值不会受到.foreach命令的影响。
  • 命令
    指定将分析其输出的一个或多个命令;生成的标记将传递给outcommands。不显示来自incommands的输出。
  • 输入字符串
    与/s一起使用。指定要分析的字符串;生成的令牌将传递给outcommands。
  • 输入文件
    与/f一起使用。指定要分析的文本文件;生成的令牌将传递给outcommands。文件名内嵌必须用引号括起来。
  • 输出命令
    指定将为每个令牌执行的一个或多个命令。每当变量字符串出现时,它将被当前标记替换。

当字符串变量出现在outcommands中时,它必须被空格包围。如果它与任何其他文本相邻(甚至是括号),则不会被当前标记值替换,除非您使用$(别名解释器)标记。当分析来自incommands、instring字符串或infile文件的输出时,任何数量的空格、制表符或回车都被视为单个分隔符。当变量出现在outcommands中时,将使用生成的每一段文本来替换它。

下面是举例 .foreach使用语句dds命令在文件中找到的每个标记myfile.txt:

0:000> .foreach /f ( place "g:\myfile.txt") { dds place }

/PS/ps标志可用于将仅某些标记传递到指定OutCommands。 例如,下面的语句将跳过myfile.txt文件中的前两个标记,然后将第三个标记传递给dds。在传递每个令牌之后,它将跳过四个令牌。结果是DDS将与第3、8、13、18和23个令牌一起使用,依此类推:

0:000> .foreach /pS 2 /ps 4 /f ( place "g:\myfile.txt") { dds place }

.while

.while标记的行为类似于c中的while关键字。
.while (Condition) { Commands }

语法要素:

  • 条件
    指定条件。如果计算结果为零,则视为假;否则为真。括号中的封闭条件是可选的。条件必须是表达式,而不是调试器命令。它将由默认表达式求值器(MASM或C++)进行评估。
  • 命令
     指定一个或多个将在条件为真时重复执行的命令。这个命令块需要用大括号括起来,即使它由一个命令组成。多个命令应该用分号分隔,但右大括号前的最后一个命令不需要后跟分号。

.printf

.printf标记的行为与c中的printf语句类似。

.printf [/D] [Option] "FormatString" [, Argument , ...]

语法要素:

  • /D
    指定格式字符串包含调试器标记语言(DML)。
  • Option
    (仅限windbg)指定windbg应将格式字符串解释为的文本消息类型。windbg为每种类型的调试器命令窗口消息分配背景和文本颜色;选择其中一个选项将使消息以适当的颜色显示。默认设置是将文本显示为普通级别的消息。
    有一下选项:
    Option 消息类型 选项对话框中的颜色的标题

    /od

    调试对象

    调试对象级别的命令窗口

    /oD

    调试对象提示符

    调试对象级别命令提示窗口

    /oe

    错误

    错误级别的命令窗口

    /on

    正常

    标准级别的命令窗口

    /op

    prompt

    级别命令提示窗口

    /oP

    提示符下注册

    提示符下注册级别的命令窗口

    /os

    符号

    符号消息级别的命令窗口

    /ov

    详细

    详细级别的命令窗口

    /ow

    警告

    警告级别的命令窗口

  • Arguments
    为指定参数的格式字符串,如printf。 指定的参数数目应与匹配的转换中的字符数FormatString。 每个自变量是将默认表达式计算器计算的表达式 (MASM 或C++)。
  • FormatString
    指定格式字符串,如printf中所示。一般来说,转换字符的工作方式与C完全相同。对于浮点转换字符,除非使用L修饰符,否则64位参数将被解释为32位浮点数字。支持%p转换字符,但它表示目标虚拟地址空间中的指针。它不能有任何修饰符,并且使用调试器的内部地址格式。支持以下附加转换字符。
    字符 自变量类型 参数 打印的文本

    %p

    ULONG64

    目标的虚拟地址空间中的指针。

    指针的值。

    %N

    DWORD_PTR (32 位或 64 位,具体取决于主机的体系结构)

    主机的虚拟地址空间中的指针。

    指针的值。 (这等效于标准 C %p 字符。)

    %ma

    ULONG64

    目标的虚拟地址空间中的以 NULL 结尾的 ASCII 字符串的地址。

    指定的字符串。

    %mu

    ULONG64

    目标的虚拟地址空间中的 NULL 终止的 Unicode 字符串的地址。

    指定的字符串。

    %msa

    ULONG64

    ANSI_STRING 结构目标的虚拟地址空间中的地址。

    指定的字符串。

    %msu

    ULONG64

    目标的虚拟地址空间中的 UNICODE_STRING 结构地址。

    指定的字符串。

    %y

    ULONG64

    目标的虚拟地址空间中的调试器符号的地址。

    包含指定的符号 (和偏移量,如果有) 的名称的字符串。

    %ly

    ULONG64

    目标的虚拟地址空间中的调试器符号的地址。

    包含名称的指定符号 (和偏移量,如果有),以及任何可用的源行信息的字符串。

默认情况下,可以使用选项参数选择的颜色设置都设置为白色背景上的黑色文本。要充分利用这些选项,必须首先使用“视图”选项打开“选项”对话框并更改调试器命令窗口消息的颜色设置。

下面的示例演示如何在格式字符串中包含DML标记。

.printf /D "Click <link cmd=\".chain /D\">here</link> to see extensions DLLs."

执行和点击链接结果如下:
Windbg命令脚本流程控制语句详解