GDB之(3)加载指定动态库文件

GDB之(3)加载指定动态库文件

Author:Once Day Date:2024年2月26日

漫漫长路,才刚刚开始…

全系列文章请查看专栏: Linux实践记录_Once-Day的博客-CSDN博客

推荐参考文档:

  • gdb 查找动态库方法_info sharedlibrary-CSDN博客
  • GDB: The GNU Project Debugger (sourceware.org)
  • GDB Documentation (sourceware.org)

文章目录

      • GDB之(3)加载指定动态库文件
        • 1. 概述
          • 1.1 跨架构调试库文件
          • 1.2 指定库文件目录
        • 2. 查看库文件加载情况
          • 2.1 查看加载的共享库信息
          • 2.2 查看进程虚拟地址映射
        • 3. 显示库源码信息
          • 3.1 查看对应函数地址源码信息
          • 3.2 设置源代码目录路径

1. 概述

GDB(GNU Debugger)是GNU开源组织发布的一个强大的Unix/Linux程序调试工具。在软件开发的调试阶段,GDB提供了程序员在程序暂停执行的情况下,查看和修改程序内部的功能。这对于跟踪错误和异常行为,在复杂的系统中尤为重要。

当程序员面对的问题是需要调试的程序依赖特定版本的共享库文件时,GDB加载指定库文件的能力就变得尤为重要。通常,系统可能有多个版本的库文件,或者开发者可能希望测试程序在使用不同版本库文件时的行为。在这种情况下,GDB就需要能够加载和使用指定的库文件。

1.1 跨架构调试库文件

一个为 x86 架构编译的 GDB 通常不能直接用来调试 ARM 架构的二进制文件和它们的 core dump。原因是 GDB 需要理解目标程序的架构(例如指令集和寄存器),而 x86 版本的 GDB 通常只理解 x86 架构。

然而,可以使用一个“交叉编译”的 GDB 版本来实现这个目标。交叉编译的 GDB 是为主机(Host)架构(例如 x86)编译的,但是被配置为理解目标架构(例如 ARM)。可以从许多各种各样的源获取交叉编译的 GDB,包括 Linux 发行版的软件源,或者交叉编译工具链的提供者(例如 Linaro 对于 ARM)。

例如,如果正在使用 Ubuntu,可以通过以下命令安装交叉编译的 GDB:

ubuntu->~:$ sudo apt-get install gdb-multiarch
ubuntu->~:$ gdb-multiarch -v
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

gdb-multiarch 是一个特殊的 gdb 版本,它支持多种架构,包括 x86 和 ARM。安装了 gdb-multiarch 后,应该就可以使用它来调试 ARM 架构的二进制文件和它们的 core dump 了。

如果不能获取一个预编译的交叉编译的 GDB,也可以从源代码自己编译一个。这需要更多的工作,但是提供了更多的灵活性。可以在 GDB 的官方文档中找到更多关于如何配置和编译 GDB 的信息。

1.2 指定库文件目录

要在GDB中加载指定的库文件,可以使用set solib-search-path命令或者set sysroot命令。这两个命令都告诉GDB在哪里查找共享库。例如,如果想要GDB只在/my/special/libs目录中搜索共享库,可以在GDB的命令行中输入:

(gdb) set solib-search-path /my/special/libs

如果库文件不在标准路径下,或者您正在交叉编译环境中工作,set sysroot命令也非常有用,它指定了系统根目录的位置,GDB会在这个位置基础上追加标准库路径来搜索库文件:

(gdb) set sysroot /my/special/sysroot

此外,GDB还支持set environment命令,可以用来设置环境变量,这在某些情况下也可以影响库文件的加载:

(gdb) set environment LD_LIBRARY_PATH /my/special/libs

在调试过程中,如果GDB提示某个库文件找不到,或者您想知道GDB实际加载了哪些库,可以使用info sharedlibrary命令查看已加载的共享库信息。

有趣的是,使用GDB加载指定库文件的功能可以让程序员在不更改系统全局设置的情况下,测试程序在不同的环境下的行为。这就像给程序穿上了“隐形斗篷”,允许它在一个临时改变的环境中运行,而不影响系统中其它程序的运行。

在使用GDB加载特定库文件时,程序员需要注意确保库文件的版本与程序兼容,否则可能会遇到诸如符号不匹配等问题。

这个过程有点像是在一个复杂的机器中更换零件,更换的零件必须要与机器其他部分相匹配,否则机器可能无法正常工作。

2. 查看库文件加载情况
2.1 查看加载的共享库信息

GDB提供了一系列命令,其中info sharedlibrary或简写的info shared命令就专门用于查看库文件加载情况。

当启动GDB并且运行了程序之后,输入info sharedlibrary命令,GDB会列出所有已加载的共享库文件,包括库文件的名字、内存中加载的地址以及是否已被加载等信息。这对于检查程序是否已经正确地加载了所有需要的动态链接库文件(Dynamic Link Libraries, DLLs),或者是否有些库文件未能正确加载是非常有用的。如果程序崩溃,可以查看是不是某个库文件没有加载或者加载了错误的版本。

除此之外,GDB还允许在特定的库文件加载或卸载时设置断点。通过break命令结合dlclosedlopen(这两者是Linux系统中用于动态加载或卸载库的函数)可以在库文件加载或卸载时暂停程序的执行,进而可以进一步调查库文件的加载细节。

使用GDB查看库文件加载情况的时候,可能会遇到一些特殊情况。例如,有时候因为权限问题或路径设置错误,库文件可能加载失败。这时,GDB的输出会帮助你快速定位问题。

有些程序员在调试过程中发现程序意外地加载了错误版本的库文件,而这通常是因为环境变量如LD_LIBRARY_PATH的设置不当。这样的小插曲提醒了程序员,环境配置是开发工作中不可忽视的一部分。

请注意,如果程序尚未开始执行(例如,如果刚刚启动 gdb 并加载了程序,但是还没有使用 run 命令开始执行),那么 info sharedlibrary 命令可能不会显示任何信息,因为此时还没有任何库被加载。在这种情况下,需要先让你的程序开始执行,然后再使用 info sharedlibrary 命令。

如下所示:

(gdb) info sharedlibrary 
From                To                  Syms Read   Shared Object Library
0x00007ffff7fc5090  0x00007ffff7fee335  Yes         /lib64/ld-linux-x86-64.so.2
0x00007ffff7db1700  0x00007ffff7f43abd  Yes         /lib/x86_64-linux-gnu/libc.so.6
2.2 查看进程虚拟地址映射

在程序开发和调试过程中,理解进程的虚拟地址空间结构是非常重要的。GDB(GNU Debugger)提供了这样的能力,让开发者能够深入了解程序的内存布局。要查看一个进程的虚拟地址映射,可以使用GDB中的 info proc mappings 命令,这个命令展示了进程的虚拟内存映射,包括每个内存段的起始地址、结束地址、大小以及相关权限等信息。

在使用GDB时,首先需要启动GDB并附加到一个正在运行的进程或启动一个新的进程。如果想要调试一个正在运行的进程,可以使用 attach process_id 命令将GDB附加到该进程,其中 process_id 是进程的ID号。如果是启动一个新的进程,可以直接使用 file 命令加载程序,然后使用 run 命令运行它。

一旦进程在GDB的控制之下,就可以输入 info proc mappings 命令来查看进程的虚拟内存布局。这个命令的输出通常会显示多个内存区域,每个区域都有其特定的用途,如堆(heap)、栈(stack)、以及用于程序代码和数据的区域。每个区域都会显示相应的地址范围和权限,权限通常包括读(r)、写(w)和执行(x),如下所示:

ubuntu->cs-test:$ sudo gdb-multiarch --args ./my-pidof bash
.............
(gdb) Quit
(gdb) break main
Breakpoint 1 at 0x17e8
(gdb) r
Starting program: /home/ubuntu/tdata/cs-test/my-pidof bash
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".Breakpoint 1, 0x00005555555557e8 in main ()
(gdb) info proc mappings 
process 3546635
Mapped address spaces:Start Addr           End Addr       Size     Offset  Perms  objfile0x555555554000     0x555555555000     0x1000        0x0  r--p   /home/ubuntu/tdata/cs-test/my-pidof0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /home/ubuntu/tdata/cs-test/my-pidof0x555555556000     0x555555557000     0x1000     0x2000  r--p   /home/ubuntu/tdata/cs-test/my-pidof0x555555557000     0x555555558000     0x1000     0x2000  r--p   /home/ubuntu/tdata/cs-test/my-pidof0x555555558000     0x555555559000     0x1000     0x3000  rw-p   /home/ubuntu/tdata/cs-test/my-pidof0x7ffff7d86000     0x7ffff7d89000     0x3000        0x0  rw-p   0x7ffff7d89000     0x7ffff7db1000    0x28000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.60x7ffff7db1000     0x7ffff7f46000   0x195000    0x28000  r-xp   /usr/lib/x86_64-linux-gnu/libc.so.60x7ffff7f46000     0x7ffff7f9e000    0x58000   0x1bd000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.60x7ffff7f9e000     0x7ffff7fa2000     0x4000   0x214000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.60x7ffff7fa2000     0x7ffff7fa4000     0x2000   0x218000  rw-p   /usr/lib/x86_64-linux-gnu/libc.so.6
......

例如,堆区域通常用于动态内存分配,栈区域用于函数调用时的局部变量和返回地址,而代码区域则包括了程序的机器代码。通过这样的映射信息,开发者能够理解各种数据在内存中的分布,并有助于诊断内存溢出、访问违规等常见的内存问题。

值得注意的是,info proc mappings 命令在不同的操作系统和GDB版本中可能会有所不同。在某些系统中,可能需要使用 maintenance info sections 命令来获取类似的内存映射信息。

此外,有时候开发者可能会遇到权限问题,这种情况下可能需要以超级用户(root)权限运行GDB。

GDB提供的虚拟地址映射查看功能是一个强大的特性,它能够帮助程序员更好地了解和调试程序的内存使用情况。

3. 显示库源码信息
3.1 查看对应函数地址源码信息

在程序调试过程中,查看程序的源码是理解程序行为和定位问题的关键。GDB(GNU Debugger)提供了丰富的命令来显示程序的源码信息,使得调试工作更加直观和高效。要在GDB中显示程序的源码,可以使用一系列相关命令,下面是几个最常用的:

  1. list(或简写为 l)命令:这是查看源码最基本的GDB命令。当在GDB中键入 list 命令时,默认情况下,GDB会显示当前断点位置的周围几行代码。也可以指定特定的行号或函数名来查看特定部分的代码,例如 list 10 将显示从第10行开始的源代码,而 list main 则会显示 main 函数周围的代码。

  2. list filename:linenumber 命令:如果想查看特定文件中特定行号的源码,可以使用这个命令。例如,list main.c:15 会显示 main.c 文件中第15行的源代码。

  3. show listsize 命令:这个命令可以用来显示或设置 list 命令一次性显示的源码行数。可以通过 set listsize number 命令来改变这个值,其中 number 是想要一次显示的行数。

  4. list *address 命令:有时可能只知道程序中某个特定地址,而想查看该地址对应的源码。使用这个命令,GDB会显示包含那个地址的源码行。

为了能够使用这些命令,程序需要包含调试信息(通常是在编译时使用 -g 选项)。没有调试信息的二进制文件会限制GDB显示源码的能力。

此外,GDB还提供了命令来控制源码的显示方式。例如,set listsize 命令可以调整每次显示的行数,而 set pagination onset pagination off 命令则可以开启或关闭分页显示,这样当输出内容过多时,可以逐页查看而不是一次性滚动过去。

使用GDB查看源码的时候,可能会遇到源码不可用的情况,这通常是由于没有编译带有调试信息的程序或源码路径不正确所致。GDB会尝试在当前目录或编译时指定的目录中查找源文件。如果源文件不在这些位置,可以使用 dir 命令来添加额外的搜索路径。

下面是一个操作示例(程序编译时要带上-g选项):

(gdb) break watchdog_get_usage 
Breakpoint 1 at 0x31e8: file block-analyze.c, line 655.
(gdb) r
Starting program: /home/ubuntu/tdata/cs-test/block-analyze.out 2000
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".Breakpoint 1, watchdog_get_usage (interval_ms=2000) at block-analyze.c:655
warning: Source file is more recent than executable.
655      */
(gdb) list
650
651     /**
652      * 获取指定CPU核的使用率, 通过读取/proc/stat文件数据实现.
653      * 对于/proc/stat文件的具体解释, 可参考Linux kernel代码: fs/proc/stat.c.
654      * 不同版本的Linux内核, /proc/stat文件的格式可能会有所不同(有问题请分析源码).
655      */
656     bool watchdog_get_usage(uint32_t interval_ms)
657     {
658         int32_t                       total_cpu, count;
659         struct watchdog_cpu           cpu_info[WATCHDOG_CPU_DATA_NUM];
(gdb) p watchdog_cpu
watchdog_cpu            watchdog_cpu_deal_data  watchdog_cpu_dump_data  watchdog_cpu_get_usage
(gdb) p watchdog_cpu_dump_data 
$1 = {void (struct watchdog_cpu *)} 0x5555555555fe <watchdog_cpu_dump_data>
(gdb) list *0x5555555555fe
0x5555555555fe is in watchdog_cpu_dump_data (block-analyze.c:113).
108     #define WATCHDOG_CPU_IDLE(cpu)  ((cpu)->idle + (cpu)->iowait)
109     #define WATCHDOG_CPU_USAGE(cpu) (WATCHDOG_CPU_TOTAL(cpu) - WATCHDOG_CPU_IDLE(cpu))
110
111     /* 格式化打印watchdog cpu中所有字段的数据 */
112     static void watchdog_cpu_dump_data(struct watchdog_cpu *cpu_info)
113     {
114         WDG_info(
115             "CPU user: %lu, nice: %lu, system: %lu, idle: %lu, iowait: %lu, irq: %lu, softirq: %lu, "
116             "steal: %lu, guest: %lu, guest_nice: %lu.\n",
117             cpu_info->user, cpu_info->nice, cpu_info->system, cpu_info->idle, cpu_info->iowait,
3.2 设置源代码目录路径

在使用GDB(GNU Debugger)进行程序调试时,有时源代码不在GDB默认的搜索路径中。为了使GDB能够找到源代码,可以使用 directory 命令来设置源代码的目录路径。这个命令告诉GDB在哪些路径下查找源文件,当源代码和可执行文件不在同一个目录下时,非常有用,或者是源代码在编译后被移动到了一个新的位置。使用 directory 命令的基本语法如下:

directory 路径

这里的“路径”指的是源代码所在的目录,可以是相对路径或绝对路径。如果有多个目录需要添加,可以用冒号(在UNIX或Linux系统中)或分号(在Windows系统中)将它们分隔开。例如:

directory /path/to/source:/another/path/to/source

当添加新的搜索路径后,GDB会在这些目录中搜索源文件。如果想要查看当前的搜索路径列表,只需要输入 directory 命令而不加任何参数。GDB将显示出当前的搜索路径。如果想清除所有已设置的源代码路径,可以使用以下命令:

directory

然后GDB会询问是否要清除所有的路径设置。确认后,GDB将只在默认的目录中搜索源文件,通常是可执行文件所在的目录。

如果源代码路径中包含了许多不再需要的目录,gdb(GNU调试器)有时可能会因为找到错误版本的源代码而导致混淆。可以按照以下步骤来纠正这种情况:

  1. 使用不带参数的 directory 命令来将源代码路径重置为其默认值。
  2. 使用带有适当参数的 directory 命令来重新添加希望包含在源代码路径中的目录,可以一次性添加所有需要的目录。

下面是一个示例演示:

(gdb) directory ../Python-c/
Source directories searched: /home/ubuntu/tdata/cs-test/../Python-c:$cdir:$cwd
(gdb) directory ../c-source/
Source directories searched: /home/ubuntu/tdata/cs-test/../c-source:/home/ubuntu/tdata/cs-test/../Python-c:$cdir:$cwd
(gdb) directory
Reinitialize source path to empty? (y or n) y
Source directories searched: $cdir:$cwd
(gdb) show directories 
Source directories searched: $cdir:$cwd






Alt

Once Day

也信美人终作土,不堪幽梦太匆匆......

如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!

(。◕‿◕。)感谢您的阅读与支持~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/705014.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

服务器权限:Error: EACCES: permission denied, open‘/Cardiac/uniquC.csv

背景&#xff1a; 我想在服务器上传一个文件uniquC.csv&#xff0c;但是服务器说我没有权限 解决方案&#xff1a; 1. 查看目前是否存在对文件夹的权限 ls -ld /Cardiac/ # your fold path 此时&#xff0c;我发现 这也意味着root也没有赋予写的权限。 2. 拿到root权限 …

Kotlin多线程

目录 线程的使用 线程的创建 例一&#xff1a;创建线程并输出Hello World Thread对象的用法 start() join() interrupt() 线程安全 原子性 可见性 有序性 线程锁 ReentrantLock ReadWriteLock 线程的使用 Java虚拟机中的多线程可以1:1映射至CPU中&#xff0c;即…

ARM 版银河麒麟桌面系统下 Qt 开发环境搭建指南

目录 前言安装Linux ARM 版 QtCreator配置 Qt Creator配置构建套件 第一个麒麟 Qt 应用程序小结 前言 在上一篇文章信创ARM架构QT应用开发环境搭建中建议大家使用 Ubuntu X86 系统作为信创 ARM 架构 QT 应用的开发环境&#xff0c;里面使用了交叉编译的方式。这对于自己的 Qt …

vue3 + vite + ts 中使用less文件全局变量

文章目录 安装依赖新建css变量文件全局引入css变量文件使用css变量 一、安装依赖 npm install less less-loader --save-dev 二、新建CSS变量文件 (1) :在根目录下的src文件中 src-> asset -> css ->glibal.less // glibal.less :root{--public_background_font_Col…

Dubbo知识点大全

“ 分布式应用场景有高并发,高可扩展和高性能的要求。还涉及到,序列化/反序列化,网络,多线程以及设计模式的问题。幸好 Dubbo 框架将上述知识进行了封装,让程序员能够把注意力放到业务上。 概念和架构 Provider:暴露服务的服务提供方Consumer:调用远程服务消费方Regist…

CUDA编程 - 用向量化访存优化 elementwise 核函数 - 学习记录

Cuda elementwise 一、简介1.1、ElementWise1.2、 float4 - 向量化访存 二、实践2.1、如何使用向量化访存2.2、Cuda elementwise - Add2.3、Cuda elementwise - Sigmoid2.3.1、简单的 Sigmoid 函数2.3.2、ElementWise Sigmoid float4&#xff08;向量化访存&#xff09; 2.4、C…

商家入驻平台怎么让资金自动分配给商家

最近很多上线了多商户电商系统的朋友咨询&#xff0c;我们平台的用户支付后&#xff0c;钱进入了我们的对公账户&#xff0c;怎样让钱在走完流程后&#xff0c;自动进入商家的账户呢&#xff1f;今天商淘云为您分享商户入驻平台自动分配给商家资金的三种方法。 首先是平台应建立…

Docker基础(一)

文章目录 1. 基础概念2. 安装docker3. docker常用命令3.1 帮助命令3.2 镜像命令3.3 容器命令3.4 其他命令 4. 使用案例 1. 基础概念 镜像&#xff08;Image&#xff09;&#xff1a;Docker 镜像&#xff08;Image&#xff09;&#xff0c;就相当于是一个 root 文件系统。比如官…

node14下运行项目报错:regeneratorRuntime is not defined

regeneratorRuntime is not defined&#xff0c;这是由于配置babel出错问题&#xff0c;由于使用了es7语法如async/await而当前babel版本过低 解决&#xff1a; npm install -D babel-plugin-transform-runtime babel-runtime 安装完成后在.babelrc文件下配置&#xff1a; &qu…

远程连接Redis

以连接阿里云上的Redis为例 1. 在阿里云安全组中开放端口 2.修改Redis启动时所用的配置文件&#xff08;redis.conf&#xff09; 2.1 修改ip地址 如图&#xff1a;将默认的本地ip bind 127.0.0.1地址改为bind 0.0.0.0 2.2 将保护模式关闭 将默认的 supervised yes 改为 n…

Transformer视频理解学习的笔记

今天复习了Transformer,ViT, 学了SwinTransformer, 还有观看了B站视频理解沐神系列串讲视频上&#xff08;24.2.26未看完,明天接着看&#xff09; 这里面更多论文见&#xff1a;https://github.com/mli/paper-reading/ B站视频理解沐神系列串讲视频下&#xff08;明天接着看&a…

认证模式~

认证方式 基于Cookie和Session的认证方式 基于Cookie和Session的认证是传统的Web应用认证机制。它依赖于HTTP协议无状态的特性&#xff0c;在客户端&#xff08;浏览器&#xff09;和服务器之间保持用户的状态。 工作原理 用户登录&#xff1a;用户通过输入用户名和密码来登…

嵌入式C语言(三)

typeof() 使用typeof可以获取一个变量或表达式的类型。 typeof的参数有两种形式&#xff1a;表达式或类型。 int i;typeof(i) j 20; --> int j 20;typeof(int *) a; -->int *a; int f(); -->typeof(f()) k;--? int k我们可以看出通过typeof获取一个变量的…

合并spark structured streaming处理流式数据产生的小文件

备注&#xff1a; By 远方时光原创&#xff0c;可转载&#xff0c;不能复制到其他平台 背景&#xff1a;做流批一体&#xff0c;湖仓一体的大数据架构&#xff0c;常见的做法就是 数据源->spark Streaming->ODS&#xff08;数据湖&#xff09;->spark streaming->…

Vue 实现页面导出A4标准大小的PDF文件,以及处理图片跨域不能正常展示的问题等

效果预览&#xff1a; 代码流程&#xff1a;首先在utils文件夹下创建htmlToPdf的js工具文件&#xff0c;然后在main.js中注册引用 htmlToPdf.js // 导出页面为PDF格式 import html2Canvas from html2canvas import JsPDF from jspdfexport default {install(Vue, options) {V…

hcia datacom课程学习(1):通信基础

1.总体框架 上图为发送方通过互联网传递信息给接收方的过程。 家用路由器会直接集成上图中的四层&#xff08;vlan&#xff0c;DHCP&#xff0c;静态路由&#xff0c;NAT&#xff0c;PPPoE&#xff09;。 2.网络性能指标 &#xff08;1&#xff09;带宽 单位时间内传输的数…

解析Hadoop三大核心组件:HDFS、MapReduce和YARN

目录 HadoopHadoop的优势 Hadoop的组成HDFS架构设计Yarn架构设计MapReduce架构设计 总结 在大数据时代&#xff0c;Hadoop作为一种开源的分布式计算框架&#xff0c;已经成为处理大规模数据的首选工具。它采用了分布式存储和计算的方式&#xff0c;能够高效地处理海量数据。Had…

pod调度策略 标签管理 资源配额与限额 全局资源配额与限额策略,

打分也是基于可调度节点进行打分资源情况. 指定多个节点,会进行覆盖其之前节点名称 --- kind: Pod apiVersion: v1 metadata:name: myhttp spec:nodeName: node-0001 # 基于节点名称进行调度containers:- name: apacheimage: myos:httpd 基于节点名称的调度策略 标签与调…

数据可视化--了解数据可视化和Excel数据可视化

目录 1.1科学可视化&#xff1a; 可视化是模式、关系、异常 1.2三基色原理&#xff1a; 三基色:红色、绿色和蓝色 1.3Excel数据可视化 1.3.1 excel数据分析-13个图表可视化技巧 1.3.2 excel数据分析-28个常用可视化图表&#xff08;video&#xff09; 1.3.3Excel可视化…

康复训练day2——2024牛客寒假集训营6

一道很好的构造题&#xff0c;受益匪浅。 链接&#xff1a;F-命运的抉择_2024牛客寒假算法基础集训营6 (nowcoder.com)​​​​​​ 题意&#xff1a; 题解 &#xff08;并查集 思维&#xff09;&#xff1a; 首先将存在1的情况特判掉&#xff0c;我们的数组的元素都是> 2的…