段、GDT、调用门学习笔记

article/2025/9/19 2:43:47

保护模式

什么是保护模式

x86 CPU的3个模式:实模式、保护模式、虚拟8086模式。

AMD64与Intel64

AMD在1999年的时候拓展了这套指令集,成为x86-64后改名叫AMD64,AMD是首先开发了64拓展,但是AMD的
64位拓展并不支持32位,后来Intel也开发了64位拓展成为Intel64并首先做到了向下兼容32位。

保护的特点

段的机制、页的机制,段页机制主要是为了保护操作系统的数据结构,比如保护系统的GDT表和IDT表,关键寄存器比如CR0寄存器

段寄存器结构

什么是段寄存器

mov dword ptr ds:[0x123456],eax(我们真正读写的地址是ds.base+0x123456)
段寄存器共有8个:ES(扩展段寄存器)、CS(代码段寄存器)、SS(堆栈段寄存器)、DS(数据段寄存器)、FS(标志段寄存器)、GS(全局段寄存器)、LDTR(局部描述符表寄存器)、TR(任务寄存器)

段寄存器结构

段寄存器有96位,但是可见部分只有16位其余80位为不可见部分
其中32位的Base、32位的Limit、16位Attribute这三部分不可见、16位的Selecter可见

段寄存器结构体定义

struct SegWent
{WORD Selector;//可见WORD Attribute;//不可见(该16位决定了段的可读可写可执行属性)DWORD Base;//不可见(该段从哪里开始执行)DWORD Limit;//不可见   //(应该是该段的大小范围)	
}

段寄存器的读写

读取段寄存器
MOV AX,ES(只能读16位可见部分)
读写LDTR指令:SLDT/LLDT
读写TR的指令: STR/LTR
写段寄存器
比如:MOV DS,AX 写时写96位 

段寄存器属性探测

段寄存器成员简介

类型	段寄存器		Selector(可变)		Attribute 		Base 	Limit
拓展段	ES		0023			可读、可写	0	0xFFFFFFFF
代码段	CS 		001B			可读、可执行	0	0xFFFFFFFF
堆栈段	SS		0023			可读、可写	0	0xFFFFFFFF
数据段	DS		0023			可读、可写	0	0xFFFFFFFF
标志段	FS		003B			可读、可写	0x7??(可变) 0xFFF
全局段      GS 		-			-		-	-	

段寄存器探测读写属性

(可以运行说明ds数据段寄存器是可读可写属性)
__asm{mov ax,dsmov ss,ds//堆栈段=数据段(ss=ds)mov dword ptr ss:[xxxx],ecx
}
(无法运行,说明cs代码段寄存器可读但不可写)
__asm{mov ax,csmov ds,ax//数据段=代码段DS=CSmov dword ptr ds:[xxx],ecx
}

段寄存探测base属性

__asm{mov ax,fsmov gs,axmov eax,gs:[0] //现在的GS段寄存器实际上就是变成了FS段寄存器=fs.base+0mov dword ptr ds:[xxx],eax//讲fs.base+0的内容给数据段的某个地址
}

段寄存器探测Limit属性

(会崩溃因为fs的Limit长度位0xFFF,越界导致崩溃)
__asm{mov ax,fsmov gs,ax//局部描述符表寄存器=标志段寄存器mov eax,gs:[0x1000]//FS[0x1000]导致越界崩溃
}	

段描述符与段选择子

GDT(全局描述符表)LDT(局部描述符表)

当执行MOV DS,AX类似的指令时,CPU会查表,根据AX的值来决定时查找GDT还是LDT,查表的什么位置和多少数据。
LDT局部描述符表目前没有在Windows操作系统中使用。

gdtr寄存器

这个寄存器存储了两个值,一个是GDT表的位置,另一个值时GDT表有多大。
GDT是全局描述符表,gdtr是寄存器,gdtr寄存器大小为48位。

gdtl

在windbg下使用r gdtl 命令可以查看GDT大小

windbg dd、dq命令

dd 内存地址,将按DWORD大小数据分为4行8列的内存数据,每个数据4个字节32位。
例子:
00000000		00000000		FFFFFFFF		12345687
dq 内存地址,将按QWORD大小数据分为2行8列的内存数据,每个数据8个字节共64位。
例子:
000000000`00000000		00cf9b00`0000fffff
L40 参数代表列出多少组数据,比如dq那么列出DWORD 40组数据,dq那么列出QWORD 40组数据。

段描述符

段寄存器有96位可见的Select只有16位,段描述符只有64位。
GDT表里面的每一个元素称为段描述符,每一个段描述符长度为8个字节。

段选择子

段选择子是一个16位的段描述符,该描述符指向了定义该段的段描述符。
MOV DS,AX其中AX就是段选择子。段选择子数据结构:
|  Index(15-3)|	TI(2)	|   RPL(0-1)|
RPL:请求特权级别
TI:0=查找GDT表	1=查找LDT表
Index:处理器将索引乘以8在加上GDT或者LDT基地址,就是要加载的,要加载GDT中那个段标识符有Index决定。

解析段选择子

假设段选择子是1B那么拆成2进制数据为:11011 其中0-1是RPL特权级值为11,TI属性为0说明查询GDT表。
Index为11十进制为3说明查找GDT表3*8的位置,因为每个段描述符长度为8字节,下标从0开始也就是3*8,位置的段描述符。

加载段表述符至段寄存器

除了MOV指令,还可以使用LES,LSS,LDS,LFS,LGS指令修改寄存器
CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要修改CS必须保证CS与EIP一起修改。
char buffer[6];
__asm{les,ecx,fword ptr ds:[buffer]//高2个字节给es,低四个字节给ecx。
}
注意:RPL<=DPL(在数值上)

GDT全局描述符表

请添加图片描述

段描述符属性P位和G位

GDT表中段描述符P位含义

P = 1 段描述符有效
P = 0 段描述符无效
说明段描述符是否有效

段寄存器Attribute结构对应段描述符位置

段寄存器Attribute结构对应GDT段描述符的12-23位的位置。
其中包含了S、DPL、P、SeqLimit、AVL、0、D/B、G等位统称为段寄存器的Attribute结构

段寄存器Base结构

段寄存器中Base值由GDT全局描述符表中3个部分的段描述符组成。
第一部分:高位高四字节24~31位
第二部分:高四字节的第0位和第7位
第三部分:低四字节的16位到31位
这三部分组成了Base结构的32位的值

段寄存器Limit结构

Limit结构由GDT段描述符中2部分的段描述符组成
第一部分:高四字节16~19位
第二部分:低0位到低15位
这两部分加起来总共是20位,表示最大范围FFFFF,段寄存器中Limit为32位。

段描述符G位作用

G位位于GDT表段描述符的高位的23位。
如果G位为0则表示段寄存器中Limit结构为字节表示最大范围为000FFFFF表述为20位。
如果G位为1则表示为4KB,那么段寄存器中Limit结构最大范围为FFFFFFFF表述为32位。

注意项

FS对应的段描述符比较特殊,查分后的值与段寄存器中的值不符合,其中牵扯到操作系统线程。

段描述符属性_S位_TYPE域

S位与Type域在段描述符中位置

S位位于高12位,Type域位于8-11位。

段描述符S位概述

快速判断:9或者F一定是数据段或代码段。
快速判断8或者E则是系统段。
S位为1则是代码段描述符或者是数据段描述符,S位为0则是系统段描述符。
GDT段描述符分类:
第一类:数据段或者代码段描述符
第二类:系统段描述符
S位一般与P位、DPL位、3位4字节组合,DPL一般都是成对出现。
如果S位为1,DPL位为11,P位为1,则dword显示数据为F。
如果S为为1,DPL位为00,P位为1,则dword显示数据为9。
可以通过上述方式快速定位S位具体是否是代码段或是数据段。

段描述符Type域概述

Type域的性质由S位决定,如果S位是1则可能是代码段或数据段。
决定是代码还是数据则由Type域决定。
使用windbg如果S位后面的Type域第11位如果为1则一定是代码段,为0则是数据段。
可以用简单方式来判断是代码段还是数据段,如果S位后面的是值小于8则是数据段否则为代码段。

段描述符Type域详解(代码段或数据段S=1)

Type域数据段位解析:
A:是否被访问过,操作系统启动的时候段描述符没有被加载过则A位为0,如果被访问过则为1。
W: 是否可写,如果为0则数据段不可写,如果为1则数据段可写。
E:拓展位,当拓展位为0则向上拓展,为1则向下拓展。向上拓展:以fs段描述符为例,如果是向上拓展则fs.base的起始地址+limit范围地址是有效的,其余地址无效。向下拓展:以fs段描述符为例,如果是向下拓展则fs.base的起始地址+limit范围地址是无效的,其余地址有效。
11位:	10位(E):		9位(W):		8位(A):
0	1		1		1
<8	向下拓展		可写		被访问过
Tyep域数据段解析:
A:(访问位)是否被访问过,与数据段描述一致。
R:(可读位)当前段是否可读,0不可读,1可读。
C:(一致位)C=1一致代码段,C=0非一致代码段。

段描述符Type域概述(系统段S=0)

Type域系统段含义描述:
11位	10位	9位	8位	描述:
0	0	0	0	Reserved
0	0	0	1	16-Bit TSS(Available)
0	0	1	0	LDT
0	0	1	1	16-Bit TSS(Busy)
0	1	0	0	16-Bit Call Gate
0	1	0	1	Task Gate
0	1	1	0	16-Bit Interrupt Gate
0	1	1	1	16-Bit Trap Gate
1	0	0	0	Reserved
1	0	0	1	32-Bit TSS(Available)
1	0	1	0	Reserved
1	0	1	1	32-Bit TSS(Busy)
1	1	0	0	32-Bit Call Gate
1	1	0	1	Reserved
1	1	1	0	32-Bit Interrupt Gate
1	1	1	1	32-Bit Trap Gate

段描述符属性_DB位

段描述符属性DB位概述

D/B位于GDT表中高位的第22位,长度为1字节。
D/B位对CS段、SS段、向下拓展的数据段有效。

DB位对CS段的影响

D=1采用32位寻址方式。
D=0采用16位寻址方式。
指令前缀67可改变寻址方式。

DB位对SS段的影响

D=1 隐式堆栈访问指令(如:PUSH POP CALL)使用32位堆栈指针寄存器ESP。
D=0 隐式堆栈访问指令(如:PUSH POP CALL)使用16位堆栈指针寄存器SP。

DB位对向下拓展的数据段

D=1段上线为4GB(32位最大范围)。
D=0段上线为64KB(16位最大范围)。

段权限检查

CPU分级

CPU分为4个级别Ring0、Ring1、Ring2、Ring3。
CPU之所以分级是比如有些特权指令只能在Ring0下运行。

如何查看程序处于几环

CPL(Current Privilege Level)当前特权级。
CPL位于CS段选择子中的RPL请求特权级别中,比如段选择子请求特权级别为11则16进制为3说明运行在3环。
SS堆栈段和CS代码段请求特权级别永远是一致的。

DPL(描述符特权级别)概述

DPL存储在段描述符中,规定了访问该段所需要的特权级被是什么。
比如在实际使用中你想访问一个GDT表那么你需要具备一个什么特权。
例子:mov DS,AX
如果AX指向的段DPL=0但是当前CS中的RPL=3那么这行指令是不会成功的。

段权限检查(数据段)

CPL<=DPL 并且RPL<=RPL(在数值上的比较)

总结:

CPL:CPU当前权限级别
DPL:如果你想访问这个GDT你应该具备什么样的权限
RPL:用什么权限去访问一个段
RPL作用:我们本可以用读写打权限去打开一个文件,为了避免出租哦有时候我们
只使用只读权限去打开。

代码跨段跳转流程

代码间的跳转(段间跳转非调用门之类的)

段间跳转,有两种情况,即要跳转一直代码段还是非一致代码段。
同时修改CS与EIP的指令:
JMP FAR /	 CALL FAR / RETF / INT / IRETED
之所以用到同时修改CS和EIP的指令是因为,指令集并没有只修改CS段的指令。
如果修改了CS段那么EIP就不准确就没有意义了。

JMP FAR 概述

JMP FAR例子: JMP 0X20:0X004183D7
0x20实际上就是段选择子拆分后:RPL=00 TI=0 Index=4。
TI=0查GDT表,Index=4找到对应的段描述符
四种情况可以跳转:代码段、调用门、TSS任务段、任务门则执行成功。
比如是数据段那么将执行失败。
权限检查:
如果是非一致代码段要求CPL==DPL并且RPL<=DPL。
如果是一致代码段,要求CPL>=DPL。	
加载段描述符:
通过上述检查后cpu会将段描述符加载到CS段寄存器中。
代码执行:
CPU将CS.Base+Offset的值写入EIP然后执行CS:EIP处的代码,段跳转结束。
JMP 0X20(BASE):0X004183D7(Offset)
0x004183D7是EIP地址。

一致代码段和非一致代码段

一致代码段(共享段):操作系统的某些代码可以被R3层直接调用且不会对操作系统造成破坏。
非一致代码段:不希望R3层直接访问的数据。

跨段跳转五步骤

1.段描述符拆分
2.查表得到段描述符
3.权限检查
4.加载段描述符
5.代码段执行

总结:

对于一致代码段:也就是共享的段
特权级别高的程序不允许访问特权级别低的数据:核心态不允许访问用户态数据。
特权级低的程序可以访问到特权级别高的数据,但特权级别不会改变:用户态还是用户态。
对于普通代码段:也就是非一致代码段:
只允许同级访问。
绝对禁止不同级别访问:核心态不是用户态,用户态不是核心态。

长调用与短调用

CALL FAR(长调用)

CALL FAR比JMP FAR要复杂,JMP并不影响堆栈,但CALL指令会影响。
JMP FAR可以实现段间的跳转,如果要实现跨段的调用就必须要学习CALL FAR,也就是长调用。

短调用CALL

指令格式: CALL 立即数/寄存器/内存
发生改变的寄存器:ESP EIP	
堆栈变化和经典CALL一样

长调用(跨段不提权)

长调用跨段不提权指的是之前CPL=3他调用的那个段选择子也是CPL=3那么就是跨段不提权。
指令格式: CALL CS:EIP(EIP是废弃的)
CALL FAR之后栈里会额外加入调用者CS,传统CALL是压入返回地址。
长调用相比短调用先入栈了调用CS在入栈了返回地址。
RETF指令:是将返回地址和调用者CS段弹栈恢复,如果用RET则不行栈将不平衡。
需要注意的是CS是一个段选择子,查询GDT表中的一个段描述符,
这个段描述符必须要是一个调用门,最终指向的代码并不是由EIP决定的。
发生改变的寄存器:EIP ESP CS

长调用(跨段并提权)

CALL FAR(跨段提权)可以通过调用门提权,提升CPL权限。
跨段并提权首先将调用者SS、调用者ESP、调用者CS、返回地址依次入栈。
发生改变的寄存器:ESP EIP CS SS
一旦涉及到权限的变化,那么堆栈也将发生切换,发生切换的堆栈,
比如从3环堆栈切换到了0环堆栈。

总结:

1.跨段调用时,一旦有权限切换,就会切换堆栈。
2.CS的权限一旦改变,SS权限也要随之改变,CS与SS等级必须一样。
3.JMP FAR只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL权限。

门描述符

请添加图片描述

调用门执行流程

指令格式:CALL CS:EIP(EIP是废弃的)
执行步骤:
1.根据CS的值,查GDT表找到对应的段描述符,这个段描述符是一个调用门。
2.在调用门描述符中存储另一个代码段段的选择子。
3.选择子指向的段 段.Base+偏移地址就是真正要执行的地址。

门描述符

S位=0(系统段)Type域=1100	这两个条件满足就是调用门这个时候他就是门描述符。
门描述符低32位里面的16到第31位是你要真正指向的段选择子。
过程:通过调用门找到段选择子。
门描述符两段偏移:
第一段偏移位于低0位到16位
第二段偏移位于高16位到31位
这两段偏移组成了一段32位的地址。
段选择子中段的Base加上两个偏移组成的32为地址就是真正执行的位置。
windows并没有使用调用门,但是可以自己构造一个调用门。

构造一个调用门(无参 提权)

高32位:0000(偏移2)EC(P为=1 DPL=11(因为门的使用在3环)S=0 Type=1100)00(不传参)
低32位:你要提权过去段选择子比如0x0008不提权直接使用这个段选择子将失败,偏移不清楚0000。

调用门代码:

int main(){char buff[6];*(DWORD*)&buff[0]=0x000000000;//EIP废弃*(WORD*)&buff[4]=0x48;//段选择子	__asm{call fword ptr[buff]//长调用并提权}getchar();return 0;
}

sgdt 指令

sgdt 指令可以在3环也可以在0环运行,他会将gdtr寄存器能容返回,获得GDT表位置。
例子:sgdt fword ptr ds:[0x0117337C]

调用门(有参)

有参数调用门,可以通过push压栈参数。

调用门总结:

1.当通过门,权限不变的时候,只会PUSH两个值:CS、返回地址新的CS的值由调用门决定。
2.当通过门,权限改变的时候,会PUSH四个值:SS ESP CS 返回地址,新的CS值由调用门决定,新的SS和ESP由TSS提供。
3.通过调用门时,要执行那个代码有调用门决定,但使用RETF返回时,由堆栈中压入的值决定,就这就是所,进门时只能按照指定路线走,出门时可以翻墙(只要改变堆栈里面的值就可以想去哪去哪)。
4.可以通过新建门的方式出去,也就是Call。

http://chatgpt.dhexx.cn/article/oYMDZ39Z.shtml

相关文章

linux内核gdt,linux内核学习之全局描述符表(GDT)(二)

在进入保护模式之前&#xff0c;我们先要学习一些基础知识。今天我们看一下全局描述符表(Global Descriptor Table, 简称GDT)。 同实模式一样&#xff0c;在保护模式下&#xff0c;对内存的访问仍然使用段地址加偏移地址。但是&#xff0c;在保护模式下&#xff0c;在每个段能够…

【IDT】 windows IDT GDT LDT

IDT&#xff1a; &#xff08;Interrupt Descrptor Table&#xff09;中断描述符表&#xff0c;用来处理中断的。 IDT的获取&#xff1a; 可以通过SIDT指令&#xff0c;它可以在内存中找到IDT&#xff0c;返回一个IDTR结构的地址。也可以通过kpcr结构获取 ISR&#xff1a; …

GDT 表与段选择子等解析

来源&#xff1a;https://blog.csdn.net/qq_37653144/article/details/82821540 https://blog.csdn.net/yeruby/article/details/39718119 https://blog.csdn.net/lindorx/article/details/89410113 全局描述表(GDT Global Descriptor Table):在保护模式下一个重要的数据结构…

GDT和GDTR

文章目录 GDTR和GDTGDT和GDTR关系GDTR的结构GDT的结构如何在MBR中建立GDT和GDTR? GDTR和GDT GDT和GDTR关系 GDT是global descriptor table&#xff0c;全局描述符表&#xff0c;它是描述符组成的一张描述符表。描述符就是段描述符它用来描述一个段的信息&#xff0c;由8个字节…

GDT,LDT,GDTR,LDTR

GDT,LDT,GDTR,LDTR 前言全局描述符表GDT局部描述符表LDT中断描述符表IDT段选择子任务寄存器TR实例1&#xff1a;访问GDT2&#xff1a;访问LDT 前言 所谓工作模式&#xff0c;是指CPU的寻址方式、寄存器大小、指令用法和内存布局等。 实模式 段基址:段内偏移地址”产生的逻辑地…

GDT和LDT详解

1.GDT 我们回顾一下实际的操作系统的内存关系: ​ 程序/进程 → 映射 段 表 逻辑地址 → segment unit 段 基 址 偏 移 地 址 虚拟地址 → page unit 页 表 物理内存地址 \text{程序/进程}\overset{段表}{\xrightarrow[\text{映射}]{}} \text{逻辑地址}\overset{段基址偏移…

GDT(全局描述符表)和LDT(局部描述符表)

Home > GDT&#xff08;全局描述符表&#xff09;和LDT&#xff08;局部描述符表&#xff09; 每个程序都有自己的LDT&#xff0c;但是同一台计算机上的所有程序共享一个GDT。LDT描述局部于每个程序的段&#xff0c;包括其代码、数据、堆栈等。GDT描述系统段&#xff0c…

什么是数据可视化?

到底什么是数据可视化&#xff1f;带你一窥究竟&#xff5e; 技术人最不该忽视可视化数据分析&#xff01; 导读&#xff1a;在这个“人人都是数据分析师”的时代&#xff0c;大企业的同学几乎都在参与数据的采集、加工与消费。数据可视化作为连接“加工——消费”的重要一环…

当下最火的中台到底是个什么鬼,看完这一篇最通俗易懂的文章后,你就会彻底明白了!...

公众号关注 「奇妙的 Linux 世界」 设为「星标」&#xff0c;每天带你提升技术视野&#xff01; 背景 自从阿里巴巴现任CEO逍遥子在2015年提出”大中台&#xff0c;小前台”战略以来&#xff0c;关于”什么是中台”&#xff0c;可谓是一石激起千层浪&#xff0c;大量文章在描述…

STD::是什么?

【&#xff23;&#xff0b;&#xff0b;】std&#xff1a;&#xff1a;是什么&#xff1f; 引例&#xff1a; #include<iostream> int main() {std::cout<<"我喜欢C";//输出一句话std::cout<<std::endl;//换行return 0; } 1.std是什么&#xff1…

C语言中 1%3,算术什么意思啊 算数什么意思

算术什么意思啊 算数什么意思以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! 算术是什么意思 数学 什么是算术和 算术和是正数的和,即绝对值的和,例如2+13.5+7,Ge个数值皆为正且相加。  主要区别于代数和,…

matlab语句temp,maxtemp什么意思 will什么意思

maxtemp什么意思 will什么意思以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! Min Temp 和 Max Temp 是什么意思 最小的Temp和最大Temp Temp: Chang用的办公软件和其他应用程序通常会临时保Cun用户的工作结果,…

html 玫瑰花

简易html 代码玫瑰花 <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>玫瑰</title><style type"text/css"> #shusheng { position: absolute; width: 100%; height: 100%; text-align: center; } &…

计算并输出所有的玫瑰花数

如果一个n位正整数等于它的n个数字的n次方和&#xff0c;则称该数为n位自方幂数。四位自方幂数称为玫瑰花数。编程计算并输出所有的玫瑰花数。 #include <stdio.h> #include <math.h>int main() {for (int i 1000; i < 10000; i){if (i pow(i%10, 4) pow((i/…

C语言代码:玫瑰花

前文 在古希腊神话中&#xff0c;玫瑰花集爱与美于一身&#xff0c;既是美神的化身&#xff0c;又溶进了爱神的血液&#xff0c;所以它所代表的含义是爱情。 我们应该用玫瑰花来表达我们的爱意&#xff0c;但是好多的恋人都是因为异地而没有办法去买一束新鲜的玫瑰去送给自己的…

七夕玫瑰花合集

图片来源&#xff1a;百度动图 一年一度的七夕又快到了&#xff0c;用Python画一朵玫瑰花送给你的那个TA吧图片。更多表白代码可以到”阿黎逸阳的代码“公众号中翻看表白合集中的文章。 一、绘制结果 1. 玫瑰花1 2. 玫瑰花2 二、画玫瑰花代码 1. 用turtle库画一朵玫瑰花版本1 #…

C++玫瑰花源码

#include "stdafx.h" #include <graphics.h> #include <conio.h> #include <math.h> // 定义全局变量 int rosesize 500; int h -250; // 定义结构体 struct DOT { double x; double y; double z; double red; // 红…

Python玫瑰花

用Python画一朵玫瑰花&#xff08;附带源码&#xff09; 需要的模块包 turtle(内置包) 源码部分 import turtle# 设置初始位置 turtle.penup() turtle.left(90) turtle.fd(200) turtle.pendown() turtle.right(90)# 花蕊 turtle.fillcolor("red") turtle.begin_fill…

C语言玫瑰花

效果图&#xff0c;如果想要更改颜色&#xff0c;可以在代码最后一行system处修改。 #include <stdio.h> #include <math.h>const int max_iterations 128; const float stop_threshold 0.01f; const float grad_step 0.01f; const float clip_far 10.0f;cons…

如何买玫瑰?

代码和任务 /*copyright(c)2015 csdn学院 *All right reserved. *文件名称&#xff1a;main.c *作者&#xff1b;张如田 *完成日期&#xff1a; *版本号&#xff1a; *任务描述&#xff1a;小慧过生日&#xff0c;小明&#xff08;小明真忙&#xff09;要买鲜花送她。每枝红玫瑰…