问题准备
题目分五关,每关都围绕main.o和phaseX.o操作,输出自己的学号。这些都是一些可重定位目标文件,满足ELF文件格式。
ELF头
段头部表:将连续的文件映射到运行时的内存段
. init:定义了_init函数,程序初始化代码会调用它
. text:已编译程序的机器代码
. rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表
. data:已初始化的全局和静态C变量
. bss:未初始化的全局和静态C变量
. symtab :一个符号表,它存放在程序中定义和引用的函数和全局变量的信息
. debug : 一个调试符号表,其条目时程序中定义的全局变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
. Line:原始C源程序的行号和.text节中机器指令之间的映射
. Strtab:一个字符串表,其内容包括 .symtab 和 .debug节中的符号表,以及节头部中的节名字。
实验过程
将linkLab20215220701.tar文件放入LInklab文件夹中,然后利用命令行“tar -xvf linkLab20215220701.tar”进行解压缩,即如下所示:
使用命令行“objdump -rd main.o > main.s”对mian.o文件进行反汇编操作并导入到文件main.s中,即如下图所示:
关卡一:修改二进制可重定位目标文件“phase1.o”的数据结构data或rodata内容使得与mian.o链接后可仅运行输出学号
使用命令行“readelf -x .rodata main.o”查看文件mian.o的数据段,其具体情况如下所示:
通过命令行“objdump -d phase1.o”可以查看到phase.o的反汇编情况,即如下图所示:
文件mian.o中的数据段情况如上图所示,并未发现函数puts(),因此,选择查看phase1.o文件,此时可使用“objdump -rd phase1.o”命令行查看重定位条目在汇编中的体现情况,以便于进行定为线索查找,其具体情况如下图所示:
上述重定位条目在汇编中的体现情况可以查看到两个重定位条目,分别对应为g_data符号和puts符号,前者为puts的参数,后者为对应调用的函数。通过上述重定位条目可以将输出内容定位到g_data有关的重定位表。
随后可以选择通过“readelf -r phase1.o”命令行查看重定位表条目,进而对相关条目进行分析,其重定位表条目情况如下所示:
通过对上图中的重定位表条目分析可知:g_data是一个符号,需要的地址偏置量为+0x12,节内偏移量为0x05,正好对应3中的字节码序列。
随后则可以通过命令行“readelf -s phase1.o”查看符号表,定位g_data符号,其具体情况如下图所示:
此时则可以查看到在编号3的data字节中,内容比较多了559bytes,随后则需要进行data字节定位。
选择使用命令行“readelf -S phase1.o”查看节头表,进而进行data节定位,以便于进行g_data符号查找操作,其具体节头表信息如下图所示:
从上图中可以查看到编号3的data节在整个ELF文件phase1.o的偏移量为0x60,再加上g-data的节内偏移量0x12,进而可以通过计算得到最终的g_data地址为0x60+0x12=0x72。
随后可以选择使用“readelf -x .data phase1.o”命令行查看ELF文件,并根据上述确定的g_data地址确定修改位置,即如下图所示:
确定修改位置后则可以进行修改操作了,此处题目要求输出学号,则可通过查询ASCII码和学号20215220701得出对应所需修改的字节为32 30 32 31 35 32 32 30 37 30 31。
确定好这些后则可以进行相应的修改操作,则可以通过命令行“hexedit phase1.o”进入二进制符号编辑界面,找到需要修改的字符串,即获取printf所输出的函数的低啊用参数地址,未进行修改前的数据字节情况如下所示:
随后则可以将字符内容修改为自身学号所对应的ASCII表中的字节,进行相应的修改后如下图所示:
修改完成后使用快捷键ctrl + w实现保存操作,随后通过快捷键ctrl + x实现退出操作,再使用命令行“readelf -x .data phase1.o”进行一次数据段操作,即如下所示:
完成修改操作后,即可通过命令行“gcc main.o phase1.o -o touch1 -no-pie”进行编译操作,随后通过命令行“./touch1”检查输出结果,从而得到输出结果如下所示:
关卡二:修改二进制可重定位目标文件“phase2.o”的数据结构data或rodata内容使得与mian.o链接后可仅运行输出学号
通过命令行“objdump -rd phase2.o > phase2.s”对phase2.o文件进行反汇编操作并将相应的反汇编代码保存至phase2.s文件中,查看其反汇编代码并对其分析可得到相应的情况。针对于myFunc函数部分的分析如下图所示:
针对于do_phase2函数的反汇编代码的分析情况则如下图所示:
通过对上面两图中的反汇编代码进行分析可知:在phase2.o文件中借助myFunc函数对g_myCharArray数组中的数值进行处理,为了能够在最后输出自身学号学要对g_myCharArray数组进行切入操作,且切入操作需要对输出的数据与处理逻辑进行逆向推理。
通过对phase2.o的反汇编代码进行整体分析可知myFunc函数能改有看到对g_myCharArray[i]的每一个元素的偏置量i%32,即g_myCharArray[i]=g_myCharArray[i]+i%32。与此同时,puts输出的首地址也是g_myCharArray[i]+0x12,
随后可使用命令行“readelf -S phase2.o”对节头表进行查看,其具体情况如下图所示:
随后可借助命令行“readelf -s phase2.o”对符号表进行查看,其具体情况如下图所示:
通过对上图分析可知该符号在common节中,属于未定义的全局符号,其所占空间大小为256个字节,即g_myCharArray属于全局数组。
随后可以选择增加phase2_patch.c文件,从而对强符号g_myCharArray进行定义操作。可以通过命令行“”对phase2_patch.c文件实现创建操作,随后进入该文件进行内容编辑,初始的编辑结果如下图所示:
注意:编辑运行时所有字符均需要处于同一行,否则会导致编译出错。
此时直接用命令行“gcc -Og phase2_patch.c -c -no-pie”对phase2_patch.c文件进行编译后,在通过命令行“gcc -no-pie phase2.o phase2_patch.o main.o -o touch2”进行编译链接后,在利用命令行“./touch2”所输出的结果属于乱码,并不是仅仅只要本人学号,其具体情况如下图所示:
确定好修改的十六进制数后,则需要确定好数据的切入位置,此时则可以通过命令行“gcc main.o phase2.o -no-pie”和“objdump -d a.out”去找最终执行文件的汇编,其主要汇编代码(myFunc函数和do_phase2函数)如下所示:
通过g_myCharArray == 0x601060与起始地址为0x601072对比分析可知切入字符前空地址为0x12,即18个“0”字符.
也可以通过命令行“objdump -rd phase2.o”查看其反汇编代码进行确定,及如下图所示:
由此可以确定切入字符需要从第19个字符开始输出,从而得知phase2_patch.c文件的切入初始位置,直接在所确定的位置上切入自身的学号后,phase2_patch.c文件情况如下所示:
注意:编辑运行时所有字符均需要处于同一行,否则会导致编译出错。
此时直接用命令行“gcc -Og phase2_patch.c -c -no-pie”对phase2_patch.c文件进行编译后,在通过命令行“gcc -no-pie phase2.o phase2_patch.o main.o -o touch2”进行编译链接后,在利用命令行“./touch2”进行运行,其运行结果如下图所示:
此时的运行结果依旧处于乱码状态,由于需要只输出学号,则需要将乱码的前11个字符转换为学号的关系。首先需要已知“A~Z”这26个字符所对应的十进制数据,其具体关系如下所示:
而针对于所需输出的学号字符串,则需要找出其对应的十进制数据与字符的转换关系,其具体关系如下表所示:
所需输出的学号字符为“20215220701”,即对应学号的十进制数据为“50 48 50 49 53 50 50 48 55 48 49”。
通过上述可以针对于乱码的前11个字符进行分析,从而可得以下结果:
①字符D在ASSCII码中的十进制数为68,字符2在ASSCII码中的十进制数为50,而68=50+18,得到50-18=32,即0x20(\x20)
②字符C在ASSCII码中的十进制数为67,字符0在ASSCII码中的十进制数为48,而67=48+19,得到48-19=29,即0x1d(\x1d)
③字符F在ASSCII码中的十进制数为70,字符2在ASSCII码中的十进制数为50,而70=50+20,得到50-20=30,即0x1e(\x1e)
④字符F在ASSCII码中的十进制数为70,字符1在ASSCII码中的十进制数为49,而70=49+21,得到49-21=28,即0x1c(\x1c)
⑤字符K在ASSCII码中的十进制数为75,字符5在ASSCII码中的十进制数为53,而75=53+22,得到53-22=31,即0x1f(\x1f)
⑥字符I在ASSCII码中的十进制数为73,字符2在ASSCII码中的十进制数为50,而73=50+23,得到50-23=27,即0x1b(\x1b)
⑦字符J在ASSCII码中的十进制数为74,字符2在ASSCII码中的十进制数为50,而74=50+24,得到50-24=26,即0x1a(\x1a)
⑧字符I在ASSCII码中的十进制数为73,字符0在ASSCII码中的十进制数为48,而73=48+25,得到48-25=23,即0x17(\x17)
⑨字符Q在ASSCII码中的十进制数为81,字符7在ASSCII码中的十进制数为55,而81=55+26,得到55-26=29,即0x1d(\x1d)
⑩字符K在ASSCII码中的十进制数为75,字符0在ASSCII码中的十进制数为48,而75=48+27,得到48-27=21,即0x15(\x15)
⑪字符M在ASSCII码中的十进制数为77,字符1在ASSCII码中的十进制数为49,而77=49+28,得到49-28=21,即0x15(\x15)
综上所述,可以推断出学号对应的十六进制字符为\x20\x1d\x1e\x1c\x1f\x1b\x1a\x17\x1d\x15\x15。
得到切入的字符串后,还需要一个在最后加上一个结束符,以便于输出学号后便结束输出。结束字符为“‘\0’”,其对应的十进制为“256”,又已知第十二个输出字符“M”在ASSCII码中的十进制数为77,对48取余后为29,则对应存入的十进制数为“227(256-29)”,转换为十六进制为“0xe3”,从而确定最终的切入字符串为“\x20\x1d\x1e\x1c\x1f\x1b\x1a\x17\x1d\x15\x15\xe3”,实现切入后phase2_patch.c文件内容如下所示:
注意,实际的phase2_patch.c文件内容需要保持在同一行,否则将会导致编译出错,即如下图所示:
注意:上图中的后方部分“0”并为完全截取。
此时直接用命令行“gcc -Og phase2_patch.c -c -no-pie”对phase2_patch.c文件进行编译后,在通过命令行“gcc -no-pie phase2.o phase2_patch.o main.o -o touch2”进行编译链接后,即可输出自身学号,具体情况如下图所示:
关卡三:修改二进制可重定位目标文件“phase3.o”的数据结构data或rodata内容使得与mian.o链接后可仅运行输出学号
直接通过命令行“gcc -no-pie main.o phase3.o -o touch3”对文件main.o和phase3.o文件进行联合编译后,直接使用命令行“./touch3”进行运行后没有任何输出,即如下图所示:
随后可以选择使用命令行“objdump -d phase3.o”查看文件phase3.o的反汇编,其具体情况如下图所示:
针对于文件phase3.o的反汇编代码可知其主要划分为myFunc1函数、myFunc2函数和do_phase3函数三个部分,其最为突出的问题的聚集点是do_phase3这个函数,其并未调用任何函数,其反汇编代码大部分是空指令,即main -> do_phase3这个过程中并未进行任何操作,从而导致最初的编译运行没有任何结果。
根据实验要求则是需要在联合编译运行后输出自身学号,即该过程也需要同关卡二中那般,调用puts函数进行输出,即需要编写相应的指令修改文本用以替换nop指令实现自身学号的输出。
随后则可以通过命令行“objdump -rd phase3.o”对重定位表进行查看操作,其具体情况如下图所示:
通过上述重定位表可以发现,其值将传给内存单元-8(%rbp)。而在myFunc1函数中,只是简单地实现puts输出,而内存单元-8(%rbp)中的数值不经过处理则绝对无法实现学号的输出,因此,程序必须依次经过①和②才可以实现,即需要先调用myFunc2函数对内存单元实现修改,且可以确定重定位位置外加偏移量为0x12,即十进制的18,再在调用myFunc1函数进行结果输出才可以实现相应的输出。 随后则可以使用命令行“objdump -d touch3”对touch3进行反汇编操作,查看其myFunc1函数和myFunc2函数,即如下所示:通过上述可以查看到myFunc2函数将0x601052这个地址存放至寄存器%rax中,随后则可以以此为目标,通过gdb调试查看该地址,即如下所示:
其中,“x/s 0x601052”表示打印指向地址0x601052的字符串,此时输出字符串大多是“x”,并非所需学号。而“x/8s 0x601052”表示以地址0x601052为起始地址返回8个单元的值。
随后可以选择使用命令行“readelf -a phase3.o”对data字节进行查看,其结果如下图所示:
通过对上图分析可知data节的偏移量为0x1a0,即输入为0x1a0后再偏移之前的0x12,即第18位后再进行输入,所得位置即为所需修改的位置。此时则可以使用命令命令行“hexedit phase3.o”进行修改操作。未进行修改前其修改位置出的数据情况如下所示:
根据关卡一可知:学号20215220701在ASSCII表中相应字符多对应的数据为32 30 32 31 35 32 32 30 37 30 31,从而得出在第19位到29位所需修改的数据为上述数据,最后以“00”最为作为终止符结束输出。其具体结果如下图所示:
修改完成后使用快捷键ctrl + w实现保存操作,随后通过快捷键ctrl + x实现退出操作。
进行完修改操作之后依旧无法输出学号,还需要使得do_phase函数能够调用myFunc1函数。该过程需要制造一个调用函数的汇编文本,即可以使用命令行“vim test3.s”生成test3.s文件并进入编辑,最后编辑结果如下所示:
其中0x12表示myFunc2函数的偏置,而0x4表示myFunc1函数的偏置,从而实现两者的调用。
随后则可以使用命令行“gcc -c test3.s”对所制造的汇编文本进行编译,在使用命令行“objdump -d test3.o”查看其反汇编代码,从而得到其字节码,即如下所示:
还需要确定do_phase3()函数在文本段text的位置,可以使用命令行“readelf -s phase3.o”进行查看,其结果如下图所示:
由上图可知其节内偏移量为0x2d。随后还需要确定文本段在ELF文件中的位置,此时可以使用命令行“readelf -S phase3.o”进行节头表查看,进而进行text节定位,其具体结果如下图所示:
综上两步操作可以确定do_phase3函数的位置应当为text节首地址+节内偏移量,即0x40+0x2d = 0x6d,即可以在命令行“hexedit phase3.o”修改页面的0x6d的后一位开始对应上命令行“objdump -d touch3”查询到的do_phase3函数的字节码,即如下所示:
随后则可以在第一个nop处,即第一个“90”处依次输入test3.o的字节码,即如下所示:
从上图对比可知自行编写的汇编代码编译后转换的内容以成功填入。随后可以使用命令行“objdump -d phase3.o”查看转换后的内容是否完成填入,此时其具体情况如下图所示:
从上图可以看到已成功将内容填写入了do_phase3函数中,最后则需要将两个e8后的相对地址进行确定即可。
现在需要对相对地址进行求解,通过命令行“objdump -d phase3.o”查看到myFunc2函数和myFunc1函数的首地址,即如下图所示:
又已知相对地址的计算为函数的首地址-call下一条指令的地址,即:
第一个相对地址为0x1b-0x36 = -27 = 0xffffffff - 0x1a = 0xffffffe5;
第一个相对地址为0x0-0x3e = -1-0x3d = 0xffffffff-0x3d = 0xffffffc2。
最后将其写入即可,可选择使用命令行“hexedit phase3.o”进行修改页面,对其进行修改写入操作,其具体结果如下图所示:
修改完成后使用快捷键ctrl + w实现保存操作,随后通过快捷键ctrl + x实现退出操作,随后可通过命令行“gcc main.o phase3.o -o touch3 -no-pie”进行编译操作,再通过命令行“./touch3”检查输出结果,从而得到输出结果如下所示:
关卡四
直接通过命令行“gcc -no-pie main.o phase4.o -o touch4”对文件main.o和phase4.o文件进行联合编译后,直接使用命令行“./touch4”进行运行后会出现“段错误(核心已转储)”,即如下图所示:
通过命令行“readelf -a phase4.o”查看phase4.o文件的ELF数据,其具体结果如下所示:
通过对上图可查看出其偏移量均为0,即不存在偏移,因此,应该需要对其偏移量进行修改,其中的一个变量是g_myCharArray,另一个变量则是temp,还存在一个puts函数,此外,g_myCharArray是位于.data节中偏移量为0(即value值)处,而temp是位于.data节中偏移量为0x14(即value值)处。
随后可以使用命令行“objdump -d phase4.o”查看phase4.o的反汇编文件,即如下图所示:
通过对上图的分析可知偏移量是要使上述三个变量或函数分别到达地址0x6、0x11、0x19。且其中0x18地址处为e8,即为call指令机器码,所以输出函数puts应该添加至此处。而另外两个变量则可以根据命令行“readelf -a phase4.o”查看phase4.o文件的ELF数据中的g_myCharArray和temp确定.data+0是g_myCharArray,而.data+0x10是temp。由此可以获知所有偏移量的修改情况。进而寻找修改地址。
随后可在通过命令行“readelf -a phase4.o”查看phase4.o文件的ELF数据中的偏移定位情况如下图所示:
从上图中的ELF数据可获知偏移量是在.rela.text开始,且在.rela.text的位置从0x250开始,所以使用ghex工具修改重定位节rel.text。此外,注意到我们的重定位节,书中有说明,每一个元素是一个结构体,共24bytes = 0x18。
在我们的重定位条目中,第一个是data+10 ==> temp符号,第二个是data+0 ==> g_myCharArray符号,第三个是puts-4 ==> puts符号。
随后可以使用命令行“hexsdit phase4.o”对相应的三个符号的数据进行修改操作,其具体操作情况如下图所示:
随后可以使用命令行“gcc -no-pie main.o phase4.o -o touch4”进行联合编译,随后使用命令行“./touch4”进行运行,从而得到运行结果如下所示:
也可以使用命令行“readelf -a phase4.o”查看phase4.o文件的ELF数据的重定位表信息,即如下图所示:
随后则需要将学号字符对应的ASCII码的数据写入其中即可。不过在此之前需要确定写入位置。
可以选择通过“readelf -r phase4.o”命令行查看重定位表条目,进而对相关条目进行分析,其重定位表条目情况如下所示:
通过对上图中的重定位表条目分析可知:g_data是一个符号,需要的地址偏置量为+0x0,节内偏移量为0x11,正好对应3中的字节码序列。
随后则可以通过命令行“readelf -s phase4.o”查看符号表,定位g_data符号,其具体情况如下图所示:
此时则可以查看到在编号3的data字节中,内容比较多了559bytes,随后则需要进行data字节定位。
选择使用命令行“readelf -S phase4.o”查看节头表,进而进行data节定位,以便于进行g_data符号查找操作,其具体节头表信息如下图所示:
从上图中可以查看到编号3的data节在整个ELF文件phase1.o的偏移量为0x60,再加上g-data的节内偏移量0x12,进而可以通过计算得到最终的g_data地址为0x60+0x0=0x60。
确定修改位置后则可以进行修改操作了,此处题目要求输出学号,则可通过查询ASCII码和学号20215220701得出对应所需修改的字节为32 30 32 31 35 32 32 30 37 30 31。
确定好这些后则可以进行相应的修改操作,则可以通过命令行“hexedit phase1.o”进入二进制符号编辑界面,找到需要修改的字符串,即获取printf所输出的函数的低啊用参数地址,未进行修改前的数据字节情况如下所示:
随后则可以将字符内容修改为自身学号所对应的ASCII表中的字节,进行相应的修改后如下图所示:
通过命令行“gcc main.o phase4.o -o touch4 -no-pie”进行编译操作,再通过命令行“./touch4”检查输出结果,从而得到输出结果如下所示:
关卡五
直接通过命令行“gcc -no-pie main.o phase5.o -o touch5”对文件main.o和phase5.o文件进行联合编译后,直接使用命令行“./touch5”进行运行后会出现“hahaha”,存在输出,但并非所需输出结果,即如下图所示:
此时无法确定时何处出现问题,可以选择从其反汇编代码处入手,即可以选择使用命令行“objdump -d touch5”查看联合编译后所得到的的touch5文件的反汇编代码,通过分析可知其主要函数为blankFunc、myFunc、do_phase,其具体情况如下所示:
通过对上述反汇编代码进行分析可以获知:do_phase主要是调用了myFunc的函数。myFunc函数主要做了一个判断,如果%rax为0则将0x601040存入%edi,并调用blankFunc函数;如果%rax为1,则将0x601030存入%edi并输出。而BlankFunc函数没什么实际作用,只是将一些值传递了一下。
随后则可以使用gdb对touch5文件进行相关的的调试操作,进入gdb后可使用命令行“layout regs”进行调试界面。
进入后需要先在myFunc函数处设置断点(“break myFunc”),随后使用“run”运行程序,随后使用“si”进行单步调试,其具体情况如下所示:
通过上图我们可以发现:在myFunc运行的时候,寄存器%rax的值为0x1,进行了跳转,并将0x601030存的内容进行了输出。
随后可以进行单步调试,直至找出程序所输出“hahaha”的地址,即如下图所示:
正常情况下只需要修改改地址的内容即可完成学号的输出操作,通过命令行“readelf -s phase5.o”查看ELF文件可以看到这个数组的大小只有0x8,无法将11位学号存储进去。
但是程序运行过程中的0x601040处定义了一个大小为0x20的数组,而这个地址正好在myFunc函数中出现过。
此时则可以将%rax的值改成0x0,使得函数不能进行跳转,随后可选择将调用blankFunc函数改成调用put函数。从汇编可以看到%rax的值是在0x601038处,所以我们将这个值改成0x0即可。
通过上述分析可知接下来需要进行如下操作:
要将%rax的值修改成0x1。汇编中存在了三个数组,查看第一个输出得到:
通过上图可以发现g_guard并不等于零,此时其值为1,而我们需要的输出的所需值为’0’。
再分别查看一下其他两个数组,其具体情况如下所示:
通过上述分析发现第三个数组是我们需要的真数组,而第二个是一个假输出,此时程序的输出就是假输出,但实验要求我们将真数组(学号)实现输出。
分析汇编即可知程序先将真数组寄存在rdi中,再将假数组寄存到rdi中,等同于假数组覆盖了我们的真数组。因此,为了实现真数组的输出,则需要将假数组与真数组的位置进行置换。
此时可以使用命令行“readelf -a phase5.o”查看ELF数据,其具体结果如下所示:
通过对上图中的信息进行分析可知.data+0对应g_myCharArray,.data+10对应g_myFakeCharArray,blankFunc-4对应g_guard。因此,只需要在hexedit中将.data+0和.data+10的偏移量20和2C互换即可改变数组的位置即可。
此外,在“readelf -a phase5.o”查看到ELF数据信息中心可以发现所需修改的位置为.rela.text,即0x340处。
随后可使用命令行“hexedit phase5.o”进行修改操作,未进行修改前的情况如下所示:
进行修改操作后的情况如下所示:
修改完成后使用快捷键ctrl + w实现保存操作,随后通过快捷键ctrl + x实现退出操作。
此时通过命令行“gcc -no-pie main.o phase5.o -o touch5”对文件main.o和phase5.o文件进行联合编译后,直接使用命令行“./touch5”进行运行的结果发生了变化,即如下图所示:
此时在gdb调试过程中可以清楚地发现所传递的地址发生了变化,即如下图所示:
随后则需要将学号字符对应的ASCII码的数据写入其中即可。不过在此之前需要确定写入位置。
可通过命令行“readelf -s phase5.o”查看符号表,定位g_myCharArray符号,其具体情况如下图所示:
选择使用命令行“readelf -S phase5.o”查看节头表,进而进行data节定位,以便于进行g_data符号查找操作,其具体节头表信息如下图所示:
通过上述两图可以发现学号字符输入的起始地址为0x60+0x10=0xa0。
确定修改位置后则可以进行修改操作了,此处题目要求输出学号,则可通过查询ASCII码和学号20215220701得出对应所需修改的字节为32 30 32 31 35 32 32 30 37 30 31。
确定好这些后则可以进行相应的修改操作,则可以通过命令行“hexedit phase1.o”进入二进制符号编辑界面,找到需要修改的字符串,未进行修改前的数据字节情况如下所示:
随后则可以将字符内容修改为自身学号所对应的ASCII表中的字节,进行相应的修改后如下图所示:
修改完成后使用快捷键ctrl + w实现保存操作,随后通过快捷键ctrl + x实现退出操作。
通过命令行“gcc main.o phase5.o -o touch5 -no-pie”进行编译操作,再通过命令行“./touch5”检查输出结果,从而得到输出结果如下所示:
标签:输出,如下,修改,实验,命令行,所示,Linklab,data From: https://www.cnblogs.com/Auion-idiot/p/17173117.html