深入理解计算机系统 - 第三章·程序的机器级表示(上)
liebian365 2024-11-27 17:07 2 浏览 0 评论
简介
这个 Lab 属于第三章 —— 程序的机器级表示,本章主要介绍了汇编的各种指令以及程序运行时栈和寄存器的变化。通过 C 的各种语法引入了对应的汇编指令,使得更易理解各种基本汇编指令以及 C 语法的底层实现。
大部分汇编指令很直白,与常用的高级语言的语法都能对应,所以很快就可以熟悉上手;switch 语句比较复杂,多了一个跳转表需要处理;浮点数有自己的寄存器和指令,在函数调用的时候需要多加注意。
本章整体能够比较流畅地看下来,每一部分讲解完成之后都会有对应的练习题帮助加深理解和记忆,感觉比大学时候轻松很多。(可能是本章依旧比较简单,没有深入太多;也有可能大学学过了,还残留着部分知识;更有可能是心态变化了,课程没有那么繁重,每天只需要用 2 小时学习即可)
疑惑点回顾
本章遇到几个印象深刻的疑惑点,先记录一下,部分还没有找到答案。
栈帧对齐
第一个疑惑点在函数调用这块(P173),主要是在被调用之前需要将栈帧按 16 字节对齐。
long Q(long x);
long P(long x, long y) {
long u = Q(y);
long v = Q(x);
return u + v;
}
以上 C 代码生成的汇编代码如下(隐去了与本处无关的部分):
P:
pushq %rbp
pushq %rbx
subq $8, %rsp
movq %rdi, %rbp
movq %rsi, %rdi
call Q@PLT
movq %rax, %rbx
movq %rbp, %rdi
call Q@PLT
addq %rbx, %rax
addq $8, %rsp
popq %rbx
popq %rbp
ret
可以发现汇编代码第 4 行 (subq $8, %rsp) 在栈中分配了 8 个字节(这一行书上的注释为:对齐栈帧),没有对应的 C 代码,同时后面也没有使用。所以当时就十分好奇为什么会生成这样的代码,即使有注释还是不明白是什么意思。
经过本地各种尝试和搜索后了解到:这样是为了将栈帧按 16 字节对齐。函数调用时, %rsp 要整除 16 才能成功,即在调用函数 P (call P) 之前, %rsp 必定是 16 的整数倍。运行 call P 时会将返回地址入栈 (P165) (但这一部分仍然算在调用者的栈帧内)。这时进入了被调用的函数 P 开始位置,但此时 %rsp 模 16 余 8 ,而在 P 保存寄存器 %rbp 和 %rsx 后(入栈 16 字节数据), %rsp 模 16 仍然余 8 ,而后续在调用函数 Q 前再无出入栈的操作,所以为了按 16 字节对齐,需要再在栈上分配 8 字节的空间 (subq $8, %rsp)。
后面在数据对齐部分 (P191) 也讲到这一知识点:
- 任何内存分配函数 (alloca, malloc, calloc , realloc) 生成的块的起始地址都必须是 16 的倍数
- 大多数函数的栈帧边界都必须是 16 字节的倍数(有一些例外)
数组分配
第二个疑惑点在缓冲区溢出这块(P195),表现形式和第一个疑惑点一样,都是在栈上多分配了一些空间,但这次表现不同,没找到原因(在这里花费好几小时,仍然没有找到答案,先暂时搁置)。
char *gets(char *s);
void echo() {
// 1. 数组的大小合法的任何数也不影响这种模式
// 2. 数组的类型换成其他基本类型也不影响这种模式
char buf[8];
gets(buf);
}
以上 C 代码生成的汇编代码如下(隐去了与本处无关的部分):
echo:
subq $24, %rsp
leaq 8(%rsp), %rdi
call gets@PLT
addq $24, %rsp
ret
可以发现虽然仍旧是按照 16 字节对齐,但是多分配了 16 字节的空间,依照前面的对齐要求,此处我们分配了 8 字节数组后已经对齐,应该是不需要再多分配空间了。
经过本地各种组合尝试后发现:只有定义了数组会这样(与数组类型无关),基本都会在数组末尾的前面留下一部分不使用的空间。猜测可能与 GCC 的栈破坏检测特性有关 (P199),但不关闭这个特性,仍旧会在金丝雀值的前面有不使用的空间。
不关闭栈破坏检测的汇编代码如下(隐去了与本处无关的部分):
echo:
subq $24, %rsp
movq %fs:40, %rax
movq %rax, 8(%rsp)
xorl %eax, %eax
movq %rsp, %rdi
call gets@PLT
movq 8(%rsp), %rax
xorq %fs:40, %rax
jne .L4
addq $24, %rsp
ret
浮点数类型转换
P208 处讲解了 GCC 生成的浮点数类型转换的汇编代码有两行,而没有使用一条汇编指令直接进行转换。作者也在书中说明不清楚为什么这样处理:
我们不太清楚 GCC 为什么会生成这样的代码,这样做既没有好处,也没有必要在 XMM 寄存器中把这个值复制一遍。
准备
可以在 官网[1] 下载 bomblab 相关的程序。
本次需要使用的程序依旧需要在 Docker 中运行,将本地 Lab 的目录挂载进容器中即可:
docker run -ti -v {PWD}:/csapp ubuntu:18.04
进入容器后需要安装 gdb :
apt-get update && apt-get -y install gdb
然后就可以愉快的开始闯关了。
闯关
本次 Lab 给了待使用的 bomb 程序及其对应的 main 部分,查看 main.c 文件,可以发现总共有 6 个字符串需要输入,必须全部正确才能正确通过这关。 Bomb Lab writeup[2] 中给了提示,我们可以使用 gdb, objdump -t, objdump -d, strings 辅助我们通过这关。(这些也在 main.c 文件开头介绍过)
首先,我们运行 gdb bomb 开始调试,进入后可以输入 help 来获取不同指令的帮助文档,也可以查看书本 P194 进行相关操作。
然后调试获取所需的 6 个字符串。
获取第一个字符串
main.c 中给出了第一个字符串将在 phase_1 中进行校验,所以我们需要在 phase_1 入口处打上断点,然后运行程序使其进入 phase_1 函数:
# 在函数 phase_1 入口处打上断点
(gdb) break phase_1
Breakpoint 1 at 0x400ee0
# 运行 bomb
(gdb) run
Starting program: /csapp/bomblab/bomb
warning: Error disabling address space randomization: Operation not permitted
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
# 程序提示输入第一个字符串,我们暂时还不知道,所以随便输入一个
idealism-xxm
# 程序停在了第一个断点处:函数 phase_1 入口
Breakpoint 1, 0x0000000000400ee0 in phase_1 ()
此时我们需要使用 disas 查看函数 phase_1 的汇编代码:
# 获取 phase_1 函数的汇编代码
(gdb) disas
Dump of assembler code for function phase_1:
=> 0x400ee0 <+0>: sub $0x8,%rsp
0x400ee4 <+4>: mov $0x402400,%esi
0x400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x400eee <+14>: test %eax,%eax
0x400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x400ef2 <+18>: callq 0x40143a <explode_bomb>
0x400ef7 <+23>: add $0x8,%rsp
0x400efb <+27>: retq
End of assembler dump.
查看汇编代码,我们发现内部调用了 strings_not_equal 来比较两个字符串是否相等,该函数接收两个参数,分别放在 %edi (我们输入的字符串的起始地址) 和 %edi (待比较字符串的起始地址) 中,如果这两个字符串不等,则会调用 explode_bomb 引爆炸弹。
mov $0x402400,%esi 表明比较字符串的起始地址为 0x402400 ,所以我们只需要查看这个地址开始的一段字符串即可获取第一个字符串。然后我们打印这个地址开始的 100 个字节对应的字符串:
(gdb) print * (char *) 0x402400@100
$1 = "Border relations with Canada have never been better.\000\000\000\000Wow! You've defused the secret stage!\000flyers"
可以获取第一个字符串为:Border relations with Canada have never been better.
获取第二个字符串
我们在上一步获取了第一个字符串,此时我们可以开始获取第二个字符串。让我们重新运行程序 (gdb bomb),在 phase_2 入口处打上断点,然后运行程序使其进入 phase_2 函数:
# 在函数 phase_2 入口处打上断点
(gdb) break phase_2
Breakpoint 1 at 0x400efc
# 运行程序
(gdb) run
Starting program: /csapp/bomblab/bomb
warning: Error disabling address space randomization: Operation not permitted
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
# 输入上一步获得的字符串
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
# 由于还不知道第二个字符串,所以随便输入以便进入函数 phase_2
idealism-xxm
Breakpoint 1, 0x0000000000400efc in phase_2 ()
此时我们需要使用 disas 查看函数 phase_2 的汇编代码:
# 获取 phase_2 函数的汇编代码
(gdb) disas
Dump of assembler code for function phase_2:
=> 0x400efc <+0>: push %rbp
0x400efd <+1>: push %rbx
0x400efe <+2>: sub $0x28,%rsp
0x400f02 <+6>: mov %rsp,%rsi
0x400f05 <+9>: callq 0x40145c <read_six_numbers>
# 省略暂时不关注的代码
...
End of assembler dump.
可以发现最开始分配了 28 个字节空间,然后调用了函数 read_six_numbers ,这个函数的参数是 %rdi (输入的字符串) 和 %rsi (刚刚分配的空间) 。根据现有条件判断应该是要从输入的字符串中读取 6 个数字,并存储在我们刚刚分配的空间中。
接下来我们就需要给函数 read_six_numbers 打断点,继续运行至函数 read_six_numbers 入口处,查看其如何将 6 个数读取出来:
# 在函数 read_six_numbers 入口处打上断点
(gdb) break read_six_numbers
Breakpoint 2 at 0x40145c
# 继续运行
(gdb) continue
Continuing.
Breakpoint 2, 0x000000000040145c in read_six_numbers ()
# 获取 read_six_numbers 函数的汇编代码
(gdb) disas
Dump of assembler code for function read_six_numbers:
=> 0x40145c <+0>: sub $0x18,%rsp
0x401460 <+4>: mov %rsi,%rdx
0x401463 <+7>: lea 0x4(%rsi),%rcx
0x401467 <+11>: lea 0x14(%rsi),%rax
0x40146b <+15>: mov %rax,0x8(%rsp)
0x401470 <+20>: lea 0x10(%rsi),%rax
0x401474 <+24>: mov %rax,(%rsp)
0x401478 <+28>: lea 0xc(%rsi),%r9
0x40147c <+32>: lea 0x8(%rsi),%r8
0x401480 <+36>: mov $0x4025c3,%esi
0x401485 <+41>: mov $0x0,%eax
0x40148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x40148f <+51>: cmp $0x5,%eax
0x401492 <+54>: jg 0x401499 <read_six_numbers+61>
0x401494 <+56>: callq 0x40143a <explode_bomb>
0x401499 <+61>: add $0x18,%rsp
0x40149d <+65>: retq
End of assembler dump.
初步观察汇编代码,发现是通过 sscanf 函数从我们输入的字符串中获取值,接下来就是判断这 6 个数字分别存在了什么位置。
书中 P120 讲解了函数的前六个入参数分别存储在 rdi, rsi, rdx, rcx, r8, r9 中, P164 讲解了从第 7 个入参开始,参数存储在栈顶。
我们先判断函数 sscanf 的入参分别是什么:
- 第一个入参 (%rdi): 后续没有改动该寄存器的地方,那么第一个入参就是我们输入的字符串
- 第二个入参 (%rsi): 可以发现 %esi 被指令 mov $0x4025c3,%esi 修改了,说明第二个参数仍然是字符串,结合函数定义可知该字符串是模式串,用于读取数字。我们使用 (gdb) print * (char *) 0x4025c3@20 查看其开始的 20 个字符,可得模式串为: %d %d %d %d %d %d
- 第三个入参 (%rdx): 可以发现 %rdx 被指令 mov %rsi,%rdx 修改了,说明第三个参数是相对传入指针偏移量为 0 的位置
- 第四个入参 (%rcx): 可以发现 %rcx 被指令 lea 0x4(%rsi),%rcx 修改了,说明第四个参数是相对传入指针偏移量为 4 的位置
- 第五个入参 (%r8): 可以发现 %r8 被指令 lea 0x8(%rsi),%r8 修改了,说明第五个参数是相对传入指针偏移量为 8 的位置
- 第六个入参 (%r9): 可以发现 %r9 被指令 lea 0xc(%rsi),%r9 修改了,说明第六个参数是相对传入指针偏移量为 12 的位置
- 第七个入参 ((%rsp)): 可以发现 (%rsp) 被指令 (lea 0x10(%rsi),%rax, mov %rax,(%rsp)) 修改了,说明第七个参数是相对传入指针偏移量为 16 的位置
- 第八个入参 (0x8(%rsp)): 可以发现 0x8(%rsp) 被指令 (lea 0x14(%rsi),%rax mov %rax,0x8(%rsp)) 修改了,说明第八个参数是相对传入指针偏移量为 20 的位置
由此可知,传入指针指向 int[6] 的数组的起始地址(假设以 arr 代表该数组),输入串是六个以空格分割的 32 位有符号数字 (int),并且这个六个数字将会按顺序存在 arr 数组中。
此时我们再看函数 phase_2 完整的汇编代码:
(gdb) disas
Dump of assembler code for function phase_2:
=> 0x400efc <+0>: push %rbp
0x400efd <+1>: push %rbx
0x400efe <+2>: sub $0x28,%rsp
0x400f02 <+6>: mov %rsp,%rsi
0x400f05 <+9>: callq 0x40145c <read_six_numbers>
0x400f0a <+14>: cmpl $0x1,(%rsp)
0x400f0e <+18>: je 0x400f30 <phase_2+52>
0x400f10 <+20>: callq 0x40143a <explode_bomb>
0x400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x400f17 <+27>: mov -0x4(%rbx),%eax
0x400f1a <+30>: add %eax,%eax
0x400f1c <+32>: cmp %eax,(%rbx)
0x400f1e <+34>: je 0x400f25 <phase_2+41>
0x400f20 <+36>: callq 0x40143a <explode_bomb>
0x400f25 <+41>: add $0x4,%rbx
0x400f29 <+45>: cmp %rbp,%rbx
0x400f2c <+48>: jne 0x400f17 <phase_2+27>
0x400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x400f30 <+52>: lea 0x4(%rsp),%rbx
0x400f35 <+57>: lea 0x18(%rsp),%rbp
0x400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x400f3c <+64>: add $0x28,%rsp
0x400f40 <+68>: pop %rbx
0x400f41 <+69>: pop %rbp
0x400f42 <+70>: retq
End of assembler dump.
调用完 read_six_numbers 函数后,我们已经将读取的六个数字按顺序存储在数组 arr 中,分别对应栈中的值: (%rsp), 0x4(%rsp), 0x8(%rsp), 0xc(%rsp), 0x10(%rsp), 0x14(%rsp) 。接下来我们依次判断每个值是多少:
- 第一个数 (arr[0]: (%rsp)): 这个数通过 cmpl $0x1,(%rsp) 进行判断,紧接着运行跳转指令 je 0x400f30 <phase_2+52> 判断是否跳过引爆炸弹的函数,表明当 (%rsp) 的值等于 1 时不会触发爆炸,所以第一个数为: 1
- 第二个数 (arr[1]: 0x4(%rsp)): 通过第一个数的判断后,成功跳转到 0x400f30 <+52> 处,这里的代码类似: int *a = arr + 1; int *b = arr + 7; ,然后跳转到 0x400f17 <+27> 处,连续三条指令表明判断 a[-1] + a[-1] 是否等于 a[0] (即判断 arr[1] == arr[0] + arr[0] 是否成立),成立时跳过引爆炸弹的函数,表明当 0x4(%rsp) 的值等于 2 时不会触发爆炸,所以第二个数为: 2
- 第三个数 (arr[1]: 0x4(%rsp)): 通过第二个数的判断后,成功跳转到 0x400f25 <+41> 处,连续三条指令表明先执行了类似 a = a + 1 的操作,然后判断 a == b 是否成立(即判断 a 是否指向了 arr + 6),成立时会接着运行并跳转至 0x400f3c <+64> 处(表明通过了所有判断,字符串合法);不成立时会紧接着跳转至 0x400f17 <+27> 处,继续前一步中的判断。所以这里其实是一个循环,只有当前数是前一个数的 2 倍时才不会引爆炸弹,所以第三个数为: 4
综上可知六个数分别为: 1, 2, 4, 8, 16, 32 ,对应的字符串为: 1 2 4 8 16 32 。
重新运行程序,依次输入前两个字符串,然后发现输出了 That's number 2. Keep going! ,表明我们第二个字符串也成功获取到了。
获取第三个字符串
我们在通过前面的步骤成功获取了前两个字符串,此时我们可以开始获取第三个字符串。让我们重新运行程序 (gdb bomb),在 phase_3 入口处打上断点,然后运行程序使其进入函数 phase_3,并获取 phase_3 汇编代码:
...
Breakpoint 1, 0x0000000000400f71 in phase_3 ()
# 获取函数 phase_3 的汇编代码
(gdb) disas
Dump of assembler code for function phase_3:
=> 0x400f43 <+0>: sub $0x18,%rsp
0x400f47 <+4>: lea 0xc(%rsp),%rcx
0x400f4c <+9>: lea 0x8(%rsp),%rdx
0x400f51 <+14>: mov $0x4025cf,%esi
0x400f56 <+19>: mov $0x0,%eax
0x400f5b <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x400f60 <+29>: cmp $0x1,%eax
0x400f63 <+32>: jg 0x400f6a <phase_3+39>
0x400f65 <+34>: callq 0x40143a <explode_bomb>
0x400f6a <+39>: cmpl $0x7,0x8(%rsp)
0x400f6f <+44>: ja 0x400fad <phase_3+106>
0x400f71 <+46>: mov 0x8(%rsp),%eax
0x400f75 <+50>: jmpq *0x402470(,%rax,8)
0x400f7c <+57>: mov $0xcf,%eax
0x400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x400f83 <+64>: mov $0x2c3,%eax
0x400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x400f8a <+71>: mov $0x100,%eax
0x400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x400f91 <+78>: mov $0x185,%eax
0x400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x400f98 <+85>: mov $0xce,%eax
0x400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x400f9f <+92>: mov $0x2aa,%eax
0x400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x400fa6 <+99>: mov $0x147,%eax
0x400fab <+104>: jmp 0x400fbe <phase_3+123>
0x400fad <+106>: callq 0x40143a <explode_bomb>
0x400fb2 <+111>: mov $0x0,%eax
0x400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x400fb9 <+118>: mov $0x137,%eax
0x400fbe <+123>: cmp 0xc(%rsp),%eax
0x400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x400fc4 <+129>: callq 0x40143a <explode_bomb>
0x400fc9 <+134>: add $0x18,%rsp
0x400fcd <+138>: retq
End of assembler dump.
0x400f43 <+0> ~ 0x400f65 <+34>: 和上一步类似,就不赘述了,主要是通过 sscanf 获取输入字符串的两个 32 位有符号整数,假设分别存储在 a (0x8(%rsp)) 和 b (0xc(%rsp)) 中。如果没有成功获取两个整数,那么就会引爆炸弹,无法继续运行。
0x400f6a <+39> ~ 0x400f6f <+44>: 主要是判断 a <= 7 是否成立,成立则可继续运行;不成立则跳转至 0x400fad <+106> 引爆炸弹,无法继续运行。
0x400f71 <+46> ~ 0x400f75 <+50>: 主要是通过 a 的值计算如何跳转,即通过 0~7 计算跳转的位置,很明显是 switch 语句的汇编代码。那么 0x402470 就是跳转表的起始位置,我们运行 print /x * 0x402470@16 查看跳转表的相关数据:
# 查看 0x402470 开始的 16 个字节
(gdb) print /x * 0x402470@16
$1 = {0x400f7c, 0x0, 0x400fb9, 0x0, 0x400f83, 0x0, 0x400f8a, 0x0, 0x400f91, 0x0, 0x400f98, 0x0, 0x400f9f, 0x0, 0x400fa6, 0x0}
按照顺序对应一下可以发现 a 对应的跳转位置及对应的代码效果关系如下:
a | 跳转位置 | 相关代码将 c 赋值为 |
0 | 0x400f7c <+57> | 0xcf(207) |
1 | 0x400fb9 <+118> | 0x137(311) |
2 | 0x400f83 <+64> | 0x2c3(707) |
3 | 0x400f8a <+71> | 0x100(256) |
4 | 0x400f91 <+78> | 0x185(389) |
5 | 0x400f98 <+85> | 0xce(206) |
6 | 0x400f9f <+92> | 0x2aa(682) |
7 | 0x400fa6 <+99> | 0x147(327) |
switch 语句执行完毕后,将运行 0x400fbe <+123> ~ 0x400fc4 <+129> ,主要是判断 b == c 是否成立,不成立则引爆炸弹,不再继续运行;成立则通过跳过引爆炸弹的操作,成功通过校验。
综上,第三个字符串有 8 个合法值:0 207, 1 311, 2 707, 3 256, 4 389, 5 206, 6 682, 7 327 。
我们重新运行程序,在输入第三个字符串时,随意输入上面 8 个字符串中的一个,控制台都会输出 Halfway there! 告诉我们第三个字符串成功获取。
- 上一篇:C# 读写文件从用户态切到内核态,到底是个什么流程?
- 下一篇:在C#中计算类型大小
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)