"ldr pc, _boot" 和 "b _boot" 有什么区别?
What's the difference between "ldr pc, _boot" and "b _boot"?
我正在检查 ARM Cortex A9 的向量 table,无意中发现了两种类型的指令:
B _boot
和
LDR 电脑,_boot
谁能给我解释一下使用 B 或 LDR 的区别?两个代码应该做同样的事情,但显然必须有区别。跟link寄存器有关系吗?
感谢您的宝贵时间!
ldr reg, symbol
从该地址的内存中加载数据到寄存器中。加载到PC是内存间接跳转。
它只会 assemble 和 link 如果 _boot
足够接近 PC 相关寻址模式可以达到它,但如果两者都在 .text
部分中,那很可能。
b symbol
设置 PC = 符号的 地址 。是直接相对跳转。
这两种情况都不涉及 link 寄存器,因为您使用的是 b
而不是 bl
或 blx
.
ldr pc, _boot
ldr pc, _boot
的另一种方法:
ldr r0, =_boot @ "global variable" address into register
ldr r0, [r0] @ load 4 bytes from that symbol address
br r0 @ and set PC = that load result
假设您的 _boot:
标签位于某些代码的前面,而不是 .word another_symbol
,这不是您想要的。您将加载一些字节的机器代码并将其用作地址。 (将 PC 设置到某处可能无效。)
但是如果你有 _boot: .word foobar
之类的东西,那就是你想要的。
b _boot
或者 b _boot
就 ldr
而言的等价物是将地址从内存加载到 PC 中。这意味着您需要在内存中保存该地址的单词,而不仅仅是 b
编码中的直接相对位移。
但是 ARM assemblers 有一个伪指令可以做到这一点:
ldr pc, =_boot
会将该标签地址加载到 PC 中,使用 PC 相对寻址模式从附近的文字池加载。或者不直接进入 PC,您可以设置 br
.
ldr r0, =_boot @ symbol address into register
br r0 @ jump to that symbol
这并不完全等价:它不是位置独立的,因为它使用的是绝对地址,而不仅仅是相对分支。
Peter 的回答涵盖了内容。这是对原因的尝试。为什么你看到有的代码用b,有的用ldr pc。
从一个简短的异常开始 table:
b reset
b handler_a
b handler_b
b handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
生成这个:
10000000 <_stack+0xff80000>:
10000000: ea000002 b 10000010 <reset>
10000004: ea000003 b 10000018 <handler_a>
10000008: ea000003 b 1000001c <handler_b>
1000000c: ea000003 b 10000020 <handler_c>
10000010 <reset>:
10000010: e3a0d902 mov sp, #32768 ; 0x8000
10000014: eafffffe b 10000014 <reset+0x4>
10000018 <handler_a>:
10000018: eafffffe b 10000018 <handler_a>
1000001c <handler_b>:
1000001c: eafffffe b 1000001c <handler_b>
10000020 <handler_c>:
10000020: eafffffe b 10000020 <handler_c>
地址 0x10000010 没有在 b(ranch) 指令中编码。相反,PC 相对偏移量是:
10000000: ea000002 -2 b 10000010 <reset>
10000004: ea000003 -1 b 10000018 <handler_a>
10000008: ea000003 +0
1000000c: ea000003 +1
10000010: e3a0d902 +2 mov sp, #32768 ; 0x8000
第一条指令用 2 作为立即数编码。对于本指令,这是以单词为单位的。 pc 在执行时提前了两条指令,因此 pc+2 将您带到重置处理程序 0x10000010.
对于 ldr 电脑:
ldr pc,reset_address
ldr pc,handler_a_address
ldr pc,handler_b_address
ldr pc,handler_c_address
reset_address : .word reset
handler_a_address: .word handler_a
handler_b_address: .word handler_b
handler_c_address: .word handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
给出:
10000000 <_stack+0xff80000>:
10000000: e59ff008 ldr pc, [pc, #8] ; 10000010 <reset_address>
10000004: e59ff008 ldr pc, [pc, #8] ; 10000014 <handler_a_address>
10000008: e59ff008 ldr pc, [pc, #8] ; 10000018 <handler_b_address>
1000000c: e59ff008 ldr pc, [pc, #8] ; 1000001c <handler_c_address>
10000010 <reset_address>:
10000010: 10000020 .word 0x10000020
10000014 <handler_a_address>:
10000014: 10000028 .word 0x10000028
10000018 <handler_b_address>:
10000018: 1000002c .word 0x1000002c
1000001c <handler_c_address>:
1000001c: 10000030 .word 0x10000030
10000020 <reset>:
10000020: e3a0d902 mov sp, #32768 ; 0x8000
10000024: eafffffe b 10000024 <reset+0x4>
10000028 <handler_a>:
10000028: eafffffe b 10000028 <handler_a>
1000002c <handler_b>:
1000002c: eafffffe b 1000002c <handler_b>
10000030 <handler_c>:
10000030: eafffffe b 10000030 <handler_c>
该指令使用字节偏移进行编码:
10000000: e59ff008 ldr pc, [pc, #8]
10000004: e59ff008 ldr pc, [pc, #8]
10000008: e59ff008 +0
1000000c: e59ff008 +4
10000010: 10000020 +8 .word 0x10000020
这样一层间接pc从pc+offset地址中的word中获取值。 pc = [0x10000010]
现在请注意这两种情况,因为我们使用标签和工具为我们完成工作,计算分支的偏移量、ldr pc、链接和放置处理程序的地址等。我们并不真正想要的东西如果可以避免,我们自己做。
现在考虑一个非常真实的情况,您从闪存启动处理器。而且您使用的是 VTOR 之前的 ARM 处理器。其中一些想要 运行 操作系统。所以你可能想要一个例外 table 用于引导加载程序(例如 u-boot 非常复杂,它在某种程度上是它自己的操作系统)。这意味着您希望 ram 位于 0x00000000。 ARM 不是芯片供应商,他们制造处理器核心,芯片供应商制造芯片并做出这些决定。一些芯片供应商会将闪存映射到 0x10000000 作为示例。为了启动并假设入口地址是 0x00000000(芯片供应商控制的东西,但通常它是 0x00000000 用于正常启动)。因此,当他们在 ARM 内核上释放复位时,我们需要将内容 0x10000000 映射到 0x00000000。内存是它自己的银行、闪存、RAM、外围设备等。芯片供应商控制所有这些,可以使其固定或使其可编程,这样就有信号和控制寄存器允许复位提取到 0x00000000 到转到也正常映射到 0x10000000 的闪存。也许是一小部分。在 boot/reset 上,对 0x00000000 的提取获取指令 a b 或 ldr pc(对于正常异常 table 使用,您有一条指令可以从 table 中退出,这意味着 b 或ldr pc).
因此对于重置,这两种方法都有效,假设有足够的闪存镜像到 0x00000000(对于成功的设计,它将是)。
如果您对这些芯片之一使用分支方法,那么当您分支到重置处理程序(重置标签)时,您的 PC 是 0x00000010 而不是 0x10000010,因此您现在需要让自己到达正确的地址。有些人只是将 0x10000000 或 0x10000000 发送到电脑作为快速破解。或者最终会使用某个标签的 ldr pc。
然后您将 运行从 Flash 开始。你会在某个时候想要一些 ram 并将 ram 映射到地址 0x00000000 (这些芯片存在,而不是闻所未闻的设计)。然后开始使用它。但是您遇到的问题是,在正确的地址 0x00000000 处不再有异常 table。这是 VTOR 之前的版本,但即使使用 VTOR,您也可能需要考虑所有这些。使用分支方法,您可能会考虑创建自己的指令,向前分支 0x10000000...如果可能的话 0xea..xxxx。我将不得不查看编码,但它是 0x10000000-8 / 4 或 0x10000000/4 - 2 或 0x04000000 - 2 并且不太适合。例如,如果我们试图达到 0x8000,那么 0x2000 - 2 将适合指令。所以我们想做一些像
0xe59ff008
0xe59ff008
0xe59ff008
0xe59ff008
0x10000000
0x10000004
0x10000008
0x1000000C
然后我们自己从 0x00000000 开始写这些,这样如果出现异常,我们会加载一个地址,该地址就是闪存中的地址。
现在我们用ldr方法代替。然后我们启动,我们没有使用地址 0x00000020 进入重置,而是进入所需的 0x10000020,我们不必弄乱该地址。如果我们将本例中的第一个 0x20 字节从 0x10000000 复制到 0x00000000,那么我们的处理程序就位了,我们不必创建任何指令或地址,工具完成了我们只需复制工作的所有工作。
Many/most 处理器使用向量 table,地址在 table 中,这些处理器中的 ARM 使用您提供的指令的固定地址。重置是第一个,所以如果你真的构建一个入口点为 0x10000000 的二进制文件,它当然不需要 table,它只需要入口点和代码开始。
reset:
mov sp,0x0x8000
...
启动后,您可以在 ram 中动态构建处理程序,生成 ldr pc 指令,通过向链接器询问该地址的代码将地址填入 table (write32(0x0004,(unsigned int )handler_a);).
其他硬件设计可以在 0x00000000 处设置闪存,在其他地方设置内存。您可能同样希望拥有一个 table 条目,而不仅仅是重置条目,并且可能希望 运行 及时更改其中一个条目。该代码将链接到 0x00000000,您现在不能更改向量 table,因为它在闪存中,因此您可能想做这样的事情(如果闪存位于 0x10000000):
ldr pc,a
ldr pc,b
ldr pc,c
ldr pc,d
a: .word 0x10000000
b: .word 0x10000004
c: .word 0x10000008
d: .word 0x1000000C
reset_address : .word reset
handler_a_address: .word handler_a
handler_b_address: .word handler_b
handler_c_address: .word handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
正在创建:
00000000 <a-0x10>:
0: e59ff008 ldr pc, [pc, #8] ; 10 <a>
4: e59ff008 ldr pc, [pc, #8] ; 14 <b>
8: e59ff008 ldr pc, [pc, #8] ; 18 <c>
c: e59ff008 ldr pc, [pc, #8] ; 1c <d>
00000010 <a>:
10: 10000000 .word 0x10000000
00000014 <b>:
14: 10000004 .word 0x10000004
00000018 <c>:
18: 10000008 .word 0x10000008
0000001c <d>:
1c: 1000000c .word 0x1000000c
00000020 <reset_address>:
20: 00000030 .word 0x00000030
00000024 <handler_a_address>:
24: 00000038 .word 0x00000038
00000028 <handler_b_address>:
28: 0000003c .word 0x0000003c
0000002c <handler_c_address>:
2c: 00000040 .word 0x00000040
00000030 <reset>:
30: e3a0d902 mov sp, #32768 ; 0x8000
34: eafffffe b 34 <reset+0x4>
00000038 <handler_a>:
38: eafffffe b 38 <handler_a>
0000003c <handler_b>:
3c: eafffffe b 3c <handler_b>
00000040 <handler_c>:
40: eafffffe b 40 <handler_c>
并在启动时将 0x0000 处的四个字复制到 0x10000000,将 0x20 处的四个字复制到 0x10000010。如果你想改变处理程序,你可以改变 0x10000010 处的单词 运行time.
使用 VTOR,如果您在 运行ning 时不关心更改矢量并且闪存位于 0x10000000,那么您可以使用 ldr pc 进行重置以使 pc 指向闪存而不是镜像在 0x00000000。然后启动程序 VTOR 指向 0x10000000。但是,如果你想动态更改地址,那么 table 需要在 ram 中,你又回到了 VTOR 之前的日子。
所以....
在指令级别,从 pc 相对偏移量加载 pc。另一个从 pc 相对偏移指向的地址加载 pc。
在系统层面,你正在处理异常table,大多数人会把它放在闪存的入口点,然后处理在地址 0x00000000 处生成一个 table,如果它是还没有。
并且如果您出于任何原因想要动态更改处理程序,那么您实际的 table(或至少您修改的那些)需要在 ram 中。
ldr pc 是最灵活的,为您添加了最多的功能,但理想情况下您必须输入更多代码。如果你有 none 个这样的问题,闪存从 0x00000000 开始,或者你不希望有任何中断或异常,所以你不需要处理程序,那么 ldr pc 需要更多的字节和更多的输入。 b(ranch) 会工作得很好。
以上只是完成每件事的一种方法,您可以使用其他方式使用工具或手动执行操作,只要它能正常工作...
我正在检查 ARM Cortex A9 的向量 table,无意中发现了两种类型的指令:
B _boot
和
LDR 电脑,_boot
谁能给我解释一下使用 B 或 LDR 的区别?两个代码应该做同样的事情,但显然必须有区别。跟link寄存器有关系吗?
感谢您的宝贵时间!
ldr reg, symbol
从该地址的内存中加载数据到寄存器中。加载到PC是内存间接跳转。
它只会 assemble 和 link 如果 _boot
足够接近 PC 相关寻址模式可以达到它,但如果两者都在 .text
部分中,那很可能。
b symbol
设置 PC = 符号的 地址 。是直接相对跳转。
这两种情况都不涉及 link 寄存器,因为您使用的是 b
而不是 bl
或 blx
.
ldr pc, _boot
ldr pc, _boot
的另一种方法:
ldr r0, =_boot @ "global variable" address into register
ldr r0, [r0] @ load 4 bytes from that symbol address
br r0 @ and set PC = that load result
假设您的 _boot:
标签位于某些代码的前面,而不是 .word another_symbol
,这不是您想要的。您将加载一些字节的机器代码并将其用作地址。 (将 PC 设置到某处可能无效。)
但是如果你有 _boot: .word foobar
之类的东西,那就是你想要的。
b _boot
或者 b _boot
就 ldr
而言的等价物是将地址从内存加载到 PC 中。这意味着您需要在内存中保存该地址的单词,而不仅仅是 b
编码中的直接相对位移。
但是 ARM assemblers 有一个伪指令可以做到这一点:
ldr pc, =_boot
会将该标签地址加载到 PC 中,使用 PC 相对寻址模式从附近的文字池加载。或者不直接进入 PC,您可以设置 br
.
ldr r0, =_boot @ symbol address into register
br r0 @ jump to that symbol
这并不完全等价:它不是位置独立的,因为它使用的是绝对地址,而不仅仅是相对分支。
Peter 的回答涵盖了内容。这是对原因的尝试。为什么你看到有的代码用b,有的用ldr pc。
从一个简短的异常开始 table:
b reset
b handler_a
b handler_b
b handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
生成这个:
10000000 <_stack+0xff80000>:
10000000: ea000002 b 10000010 <reset>
10000004: ea000003 b 10000018 <handler_a>
10000008: ea000003 b 1000001c <handler_b>
1000000c: ea000003 b 10000020 <handler_c>
10000010 <reset>:
10000010: e3a0d902 mov sp, #32768 ; 0x8000
10000014: eafffffe b 10000014 <reset+0x4>
10000018 <handler_a>:
10000018: eafffffe b 10000018 <handler_a>
1000001c <handler_b>:
1000001c: eafffffe b 1000001c <handler_b>
10000020 <handler_c>:
10000020: eafffffe b 10000020 <handler_c>
地址 0x10000010 没有在 b(ranch) 指令中编码。相反,PC 相对偏移量是:
10000000: ea000002 -2 b 10000010 <reset>
10000004: ea000003 -1 b 10000018 <handler_a>
10000008: ea000003 +0
1000000c: ea000003 +1
10000010: e3a0d902 +2 mov sp, #32768 ; 0x8000
第一条指令用 2 作为立即数编码。对于本指令,这是以单词为单位的。 pc 在执行时提前了两条指令,因此 pc+2 将您带到重置处理程序 0x10000010.
对于 ldr 电脑:
ldr pc,reset_address
ldr pc,handler_a_address
ldr pc,handler_b_address
ldr pc,handler_c_address
reset_address : .word reset
handler_a_address: .word handler_a
handler_b_address: .word handler_b
handler_c_address: .word handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
给出:
10000000 <_stack+0xff80000>:
10000000: e59ff008 ldr pc, [pc, #8] ; 10000010 <reset_address>
10000004: e59ff008 ldr pc, [pc, #8] ; 10000014 <handler_a_address>
10000008: e59ff008 ldr pc, [pc, #8] ; 10000018 <handler_b_address>
1000000c: e59ff008 ldr pc, [pc, #8] ; 1000001c <handler_c_address>
10000010 <reset_address>:
10000010: 10000020 .word 0x10000020
10000014 <handler_a_address>:
10000014: 10000028 .word 0x10000028
10000018 <handler_b_address>:
10000018: 1000002c .word 0x1000002c
1000001c <handler_c_address>:
1000001c: 10000030 .word 0x10000030
10000020 <reset>:
10000020: e3a0d902 mov sp, #32768 ; 0x8000
10000024: eafffffe b 10000024 <reset+0x4>
10000028 <handler_a>:
10000028: eafffffe b 10000028 <handler_a>
1000002c <handler_b>:
1000002c: eafffffe b 1000002c <handler_b>
10000030 <handler_c>:
10000030: eafffffe b 10000030 <handler_c>
该指令使用字节偏移进行编码:
10000000: e59ff008 ldr pc, [pc, #8]
10000004: e59ff008 ldr pc, [pc, #8]
10000008: e59ff008 +0
1000000c: e59ff008 +4
10000010: 10000020 +8 .word 0x10000020
这样一层间接pc从pc+offset地址中的word中获取值。 pc = [0x10000010]
现在请注意这两种情况,因为我们使用标签和工具为我们完成工作,计算分支的偏移量、ldr pc、链接和放置处理程序的地址等。我们并不真正想要的东西如果可以避免,我们自己做。
现在考虑一个非常真实的情况,您从闪存启动处理器。而且您使用的是 VTOR 之前的 ARM 处理器。其中一些想要 运行 操作系统。所以你可能想要一个例外 table 用于引导加载程序(例如 u-boot 非常复杂,它在某种程度上是它自己的操作系统)。这意味着您希望 ram 位于 0x00000000。 ARM 不是芯片供应商,他们制造处理器核心,芯片供应商制造芯片并做出这些决定。一些芯片供应商会将闪存映射到 0x10000000 作为示例。为了启动并假设入口地址是 0x00000000(芯片供应商控制的东西,但通常它是 0x00000000 用于正常启动)。因此,当他们在 ARM 内核上释放复位时,我们需要将内容 0x10000000 映射到 0x00000000。内存是它自己的银行、闪存、RAM、外围设备等。芯片供应商控制所有这些,可以使其固定或使其可编程,这样就有信号和控制寄存器允许复位提取到 0x00000000 到转到也正常映射到 0x10000000 的闪存。也许是一小部分。在 boot/reset 上,对 0x00000000 的提取获取指令 a b 或 ldr pc(对于正常异常 table 使用,您有一条指令可以从 table 中退出,这意味着 b 或ldr pc).
因此对于重置,这两种方法都有效,假设有足够的闪存镜像到 0x00000000(对于成功的设计,它将是)。
如果您对这些芯片之一使用分支方法,那么当您分支到重置处理程序(重置标签)时,您的 PC 是 0x00000010 而不是 0x10000010,因此您现在需要让自己到达正确的地址。有些人只是将 0x10000000 或 0x10000000 发送到电脑作为快速破解。或者最终会使用某个标签的 ldr pc。
然后您将 运行从 Flash 开始。你会在某个时候想要一些 ram 并将 ram 映射到地址 0x00000000 (这些芯片存在,而不是闻所未闻的设计)。然后开始使用它。但是您遇到的问题是,在正确的地址 0x00000000 处不再有异常 table。这是 VTOR 之前的版本,但即使使用 VTOR,您也可能需要考虑所有这些。使用分支方法,您可能会考虑创建自己的指令,向前分支 0x10000000...如果可能的话 0xea..xxxx。我将不得不查看编码,但它是 0x10000000-8 / 4 或 0x10000000/4 - 2 或 0x04000000 - 2 并且不太适合。例如,如果我们试图达到 0x8000,那么 0x2000 - 2 将适合指令。所以我们想做一些像
0xe59ff008
0xe59ff008
0xe59ff008
0xe59ff008
0x10000000
0x10000004
0x10000008
0x1000000C
然后我们自己从 0x00000000 开始写这些,这样如果出现异常,我们会加载一个地址,该地址就是闪存中的地址。
现在我们用ldr方法代替。然后我们启动,我们没有使用地址 0x00000020 进入重置,而是进入所需的 0x10000020,我们不必弄乱该地址。如果我们将本例中的第一个 0x20 字节从 0x10000000 复制到 0x00000000,那么我们的处理程序就位了,我们不必创建任何指令或地址,工具完成了我们只需复制工作的所有工作。
Many/most 处理器使用向量 table,地址在 table 中,这些处理器中的 ARM 使用您提供的指令的固定地址。重置是第一个,所以如果你真的构建一个入口点为 0x10000000 的二进制文件,它当然不需要 table,它只需要入口点和代码开始。
reset:
mov sp,0x0x8000
...
启动后,您可以在 ram 中动态构建处理程序,生成 ldr pc 指令,通过向链接器询问该地址的代码将地址填入 table (write32(0x0004,(unsigned int )handler_a);).
其他硬件设计可以在 0x00000000 处设置闪存,在其他地方设置内存。您可能同样希望拥有一个 table 条目,而不仅仅是重置条目,并且可能希望 运行 及时更改其中一个条目。该代码将链接到 0x00000000,您现在不能更改向量 table,因为它在闪存中,因此您可能想做这样的事情(如果闪存位于 0x10000000):
ldr pc,a
ldr pc,b
ldr pc,c
ldr pc,d
a: .word 0x10000000
b: .word 0x10000004
c: .word 0x10000008
d: .word 0x1000000C
reset_address : .word reset
handler_a_address: .word handler_a
handler_b_address: .word handler_b
handler_c_address: .word handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
正在创建:
00000000 <a-0x10>:
0: e59ff008 ldr pc, [pc, #8] ; 10 <a>
4: e59ff008 ldr pc, [pc, #8] ; 14 <b>
8: e59ff008 ldr pc, [pc, #8] ; 18 <c>
c: e59ff008 ldr pc, [pc, #8] ; 1c <d>
00000010 <a>:
10: 10000000 .word 0x10000000
00000014 <b>:
14: 10000004 .word 0x10000004
00000018 <c>:
18: 10000008 .word 0x10000008
0000001c <d>:
1c: 1000000c .word 0x1000000c
00000020 <reset_address>:
20: 00000030 .word 0x00000030
00000024 <handler_a_address>:
24: 00000038 .word 0x00000038
00000028 <handler_b_address>:
28: 0000003c .word 0x0000003c
0000002c <handler_c_address>:
2c: 00000040 .word 0x00000040
00000030 <reset>:
30: e3a0d902 mov sp, #32768 ; 0x8000
34: eafffffe b 34 <reset+0x4>
00000038 <handler_a>:
38: eafffffe b 38 <handler_a>
0000003c <handler_b>:
3c: eafffffe b 3c <handler_b>
00000040 <handler_c>:
40: eafffffe b 40 <handler_c>
并在启动时将 0x0000 处的四个字复制到 0x10000000,将 0x20 处的四个字复制到 0x10000010。如果你想改变处理程序,你可以改变 0x10000010 处的单词 运行time.
使用 VTOR,如果您在 运行ning 时不关心更改矢量并且闪存位于 0x10000000,那么您可以使用 ldr pc 进行重置以使 pc 指向闪存而不是镜像在 0x00000000。然后启动程序 VTOR 指向 0x10000000。但是,如果你想动态更改地址,那么 table 需要在 ram 中,你又回到了 VTOR 之前的日子。
所以....
在指令级别,从 pc 相对偏移量加载 pc。另一个从 pc 相对偏移指向的地址加载 pc。
在系统层面,你正在处理异常table,大多数人会把它放在闪存的入口点,然后处理在地址 0x00000000 处生成一个 table,如果它是还没有。
并且如果您出于任何原因想要动态更改处理程序,那么您实际的 table(或至少您修改的那些)需要在 ram 中。
ldr pc 是最灵活的,为您添加了最多的功能,但理想情况下您必须输入更多代码。如果你有 none 个这样的问题,闪存从 0x00000000 开始,或者你不希望有任何中断或异常,所以你不需要处理程序,那么 ldr pc 需要更多的字节和更多的输入。 b(ranch) 会工作得很好。
以上只是完成每件事的一种方法,您可以使用其他方式使用工具或手动执行操作,只要它能正常工作...