【GreenHills】GHS的Run-Time检查功能

news2024/12/22 19:46:15

【更多软件使用问题请点击亿道电子官方网站】

一、文档背景

在编写代码时,内存检查的重要性不容忽视。内存是程序运行的核心资源之一,负责存储程序运行所需的数据。由于内存资源的有限性和操作的复杂性,内存错误常常会直接导致程序崩溃、性能下降甚至安全漏洞的产生,因此,内存检查对于确保程序的稳定性、性能和安全性具有至关重要的作用。那么工程师对于这样的问题,经常处理起来很头疼。因为大量的变量定义、指针使用以及内存的分配和释放,导致工程师可能需要一个一个地去排查。这样的工作量无疑是十分庞大的。可能会严重耽误项目进展。

二、 问题分析

为了更好的规避这样的问题,那么我们就需要借助一下Run-Time Check功能。"Run-Time Check" 是一种在程序运行时执行的检查机制,旨在帮助开发人员识别和修复可能的问题,如内存泄漏、数组越界、空指针引用等。这些检查通常在开发或调试阶段启用,以帮助捕获和定位潜在的错误。GHS就存在这样的功能,可以通过下面的内容来学习GHS的Run-Time Check功能要如何使用。

(注意:因为GHS的Run-time检查是Build选项。所以,所有的选项都需要重新构建后,配置选项才会真正地生效。)

三、 GHS的Run-Time Error Check的使用

  1. Run-Time Error Check是GHS中用于控制运行时错误检查的一个选项,这在调试会话期间可能很有帮助,但会增加应用程序的大小并降低速度。这个选项有多个检查类型,可以自由地搭配不同的检查类型去实现在代码编写上存在的内存隐患。

  2. Run-Time Error Check有哪些选项并且要如何配置

    1. Run-Time Error Check选项主要针对的检查类型为:

      1. Assignment Bounds (-check=assignbound) :赋值范围检查

      2. Array Bounds(-check=bounds):数组越界检查

      3. Case Label Bounds(-check=switch):Case标签边界检查

      4. Divide by Zero(-check=zerodivide):除零操作检查

      5. Nil Pointer Dereference(-check=nilderef):空指针解引用检查

      6. Write to Watchpoint(-check=watch):地址写入检查

    2. 图3-1中可以在Value一栏中表示的是该检查类型的使能情况。“+”表示使能该检查类型;“-”表示失能该检查类型;“.”表示清除该检查类型的值。工程师可以通过直接检查value栏中图标进行逐一修改。也可以通过下面"All: - . +"去对于所有的类型进行一键配置。

图3-1

(三)不同选项的具体分析和实际效果

1.Assignment Bounds (-check=assignbound)赋值范围检查:如果赋给整型或枚举对象的值超出该对象的范围,则生成错误。(例如,将long long赋值给int对象。)只有当枚举值小于最小枚举值或大于最大枚举值时,才会发出对枚举赋值的错误。当启用这种检查时,诊断68 (integer_sign_change)和诊断69 (integer_truncated)的某些实例将被发出默认的严重级别warning,而不是remark。并且在出现该类型的错误的时候,报错信息为 Assignment out of range of integral/enum type(赋值超出整型/枚举类型的范围)和Value outside of type(类型之外的值)。

补充:(注意:对于变量直接赋值的时候,如果超过了赋值范围,该检查不会进行报错。初步推断,该检测仅限于变量使用过程中出现的赋值问题)

例子:在程序中定义了一个char类型的变量temp。在代码中有一个Add的函数,可以看到这个函数所有的结果都被直接加上300。所以,该函数的返回值一定的大于300的。而接收返回值的temp变量的赋值范围是-128~127。

a.没启用Assignment Bounds检查的时候,在Debug运行过程中的结果如图3-2-1-1。代码是正常运行的,没有显示任何的运行错误。

图3-2-1-1

b.启用Assignment Bounds检查的时候,在Debug运行过程中的结果如图3-2-1-2。在运行代码后,在 输出窗口会提示“Stopped by runtime error: Assignment out of range of integral/enum type on line 6 of function main in file hello.c”,提示在hello.c文件中的第6行的main函数中存在赋值越界的报错。

图3-2-1-2

2.Array Bounds (-check=bounds)数组越界检查:如果使用无效索引访问数组,则生成错误。只检查在编译时边界已知的数组;不检查不完整和可变长度数组。编译器分别检查每个单独的数组索引。在运行的时候检查到这个错误会提示:Array index out of bounds(数组索引越界)

例子:在工程中定义了一个int类型的二维数组temp[2][3]。在下面的赋值语句中可以看到对于temp[0][3]进行赋值,这个赋值语句是不对的,数组定义了3列,则最大的列索引值为2。这个语句的索引值为3。这样就会产生数组越界的情况。

a.没启用Array Bounds检查的时候,在Debug运行过程中的结果如图3-2-2-1。运行过程中没有出现任何报错信息。

图3-2-2-1

b.启用Array Bounds检查的时候,在Debug运行过程中的结果如图3-2-2-2。在输出窗口中可以看到报错信息:“Stopped by runtime error: Array index out of bounds on line 8 of function main in file hello.c”。表示在hello.c文件中的第8行的语句存在数组越界的情况。

图3-2-2-2

3.Case Label Bounds(-check=switch):Case标签边界检查:如果开关表达式与任何大小写标签不匹配,则生成错误。当使用默认的case标签时,此操作不适用。即在Switch语句中输入值和所有的case值均不匹配就会产生错误。但是,如果Switch语句中有default语句的话,该选项就不会生效。如果检查到这个类型的错误会提示的信息为:Case/switch index out of bounds(Case/switch索引越界)

例子:在工程中定义了一个int类型的变了num。然后,编写了一个Switch语句,num作为表达式进行输入。switch语句中有3个case值,分别是0、1、2。而num初始化的时候赋值为3。在代码运行的时候就会出现没有对应的case值匹配的问题。

a.没启用Case Label Bounds检查的时候,在Debug运行过程中的结果如图3-2-3-1。运行过程中没有出现任何报错信息。

图3-2-3-1

b.启用Case Label Bounds检查的时候,在Debug运行过程中的结果如图3-2-3-2。在输出窗口中就可以看到报错信息:“Stopped by runtime error: Case/switch index out of bounds on line 19 of function main in file hello.c ”,表示文件hello.c中函数main的第19行索引越界。

图3-2-3-2

c.Divide by Zero(-check=zerodivide)除零操作检查:检查代码中是否发生了除零操作,如果出现了除零操作操作,就会终止程序。该选项同时会记录整数除法和浮点除法。在运行的时候检查到这个错误会提示:Divide by 0(除以0)

例子:在代码中定义了一个char类型的变量temp,并且赋值为0。定义了一个int类型的变量num,并且赋值为3。然后,temp会被作为除数,num作为被除数进行计算。因为temp为0,所以在运算过程中就会出现除零操作。

4.没启用Divide by Zero检查的时候,在Debug运行过程中的结果如图3-2-4-1。运行过程中没有出现任何报错信息。

图3-2-4-1

a.启用Divide by Zero检查的时候,在Debug运行过程中的结果如图3-2-4-2。在输出窗口中可以看到报错信息:“Stopped by runtime error: Divide by 0 on line 5 of function main in file hello.c”,表示hello.c中函数main的第5行发现除零的操作。

图3-2-4-2

5.Nil Pointer Dereference(-check=nilderef)空指针解引用检查:对空指针的解引用产生错误。在运行的时候检查到这个错误会提示:Nil pointer dereference(空指针解引用)

例子:在代码中定义了一个int类型的指针ptr,并赋值NULL,即为空指针。然后,定义了一个int类型的变量value,并将指针ptr赋值给到变量。那么在这里就会出现错误地将一个指针变量赋值为NULL,而后续使用该指针传递,就会发生空指针解引用。

a.没启用Nil Pointer Dereference检查的时候,在Debug运行过程中的结果如图3-2-5-1。运行过程中没有出现任何报错信息。

图3-2-5-1

b.启用Nil Pointer Dereference检查的时候,在Debug运行过程中的结果如图3-2-5-2。

图3-2-5-2

6.Write to Watchpoint(-check=watch)地址写入检查:检查代码以监视对内存中任意地址的写入。当您的目标不支持硬件断点时使用此检查。可以使用watchpoint命令在运行时指定观察点地址。在运行的时候检查到该操作时会提示:Write to watchpoint(写入观察点)

例子:在代码中,定义了一个int类型的指针ptr,并且让指针指向0x200F0FF这个地址。然后,对于指针进行赋值。这样就会对于0x200F0FF产生一个写操作了。

a.在启用了Write to Watchpoint这个选项后,在下面的输出窗口中输入"watchpoint 0x200F0FF",在0x200F0FF设置一个观察点。在运行过程中,一旦对于0x200F0FF这个出现了写操作就会停止运行。并输出相关信息。(如图3-2-6-1)

图3-2-6-1

以上就是对于在代码本身编写上存在的内存问题进行的检查机制,这些类型可以有效地将代码编写造成的内存问题进行有效的规避。并且为了使用运行时错误检查,您必须能够在不触发中断的情况下单步执行您的目标。

四、GHS的Run-Time Memory Checks的使用

(一)Run-Time Memory Checks可以跟踪涉及误用标准库函数malloc()和free()的bug,并且会检查错误地释放了没有使用malloc()分配的内存,或者尝试使用已经释放的内存的情况,以确保进程不会以不可预测的方式运行。

(二)Run-Time Memory Checks的选项(如图4-2)

图4-2-1

1.None (-check=nomemory) -- 禁用所有由内存检查启用的检查。这个是Run-Time Memory Checks选项的默认值。

2.General (-check=alloc)(仅支持内存分配 ): 选择一个特殊版本的内存分配库例程,包括 malloc()函数和 free()函数。这些库例程会检查到有限的内存分配问题,例如释放一个尚未分配的对象。请注意,这个设置等价于 -malloc_version=memcheck。这种形式的运行时内存检查将大大增加堆大小,并略微降低进程速度。主要检查的问题有:

a.Memory that has not been allocated with malloc() is subsequently freed.(未使用malloc()分配的内存随后被释放)

 b.The same area of memory is freed twice.(同一区域的内存被释放两次)

 c.Recently freed memory has been written to (may be detected when calls to the malloc               library are made).(最近释放的内存已经被写入(可能在调用malloc库时被检测到))

3.Intensive (-check=memory):在用户应用程序的每次指针解引用时引入额外的代码,以检查包括由Run-Time Error Check->Nil Pointer Dereference(-check=nilderef)选项检查到的各种问题在内的一系列内存分配问题。此设置隐含了General (-check=alloc),因为需要该选项所选的特定库例程。要对应用程序的一小部分进行完整的内存检查,请在编译相关文件时指定 -check=memory,或者在源代码中使用 #pragma ghs check=memory,并在链接整个程序时指定 -check=alloc。主要检查的问题有:

  • Memory that has not been allocated with malloc () is subsequently freed.(未使用malloc()分配的内存随后被释放)

  • The same area of memory is freed twice.(同一区域的内存被释放两次。)

  • Recently freed memory has been written to.(最近释放的内存已被写入)

  • Attempts are made to dereference NULL.(尝试解引用NULL)

  • Accesses are attempted past the end of a heap-allocated data structure, causing array overflowerrors.(访问超出了堆分配的数据结构的末尾,导致数组溢出错误。)

4.下面两个例子展示了出现内存错误的现象之一

  • 使用一个同一区域的内存被释放两次的代码,在开启了General (-check=alloc)选项后,现象如图4-2-2。

#include <stdio.h>  
#include <stdlib.h>  
  
int main() {  
    int *ptr = NULL; //malloc(sizeof(int)); // 第一次分配内存
    int value = *ptr;
    if (ptr == NULL) {  
        perror("malloc failed");  
        return 1;  
    }  
  
    *ptr = 42; // 初始化内存  
  
    free(ptr); // 第一次释放内存  
    //ptr = NULL; // 将指针设为NULL,防止悬挂指针  
  
    // 错误的部分:尝试第二次释放同一块内存  
    free(ptr); // 此时ptr为NULL,但假设它不是NULL  

    return 0;  
}

图4-2-2

  • 使用一个会产生访问超出了堆分配的数据结构的末尾,导致数组溢出错误的代码,在开启了Intensive (-check=memory)这个选项后,现象如图4-4-2所示

  • #include <stdio.h>  
    #include <stdlib.h>  
      
    int main() {  
        // 分配一个大小为5的整数数组  
        int *array = (int *)malloc(5 * sizeof(int));  
        if (array == NULL) {  
            perror("Memory allocation failed");  
            return EXIT_FAILURE;  
        }  
      
        // 初始化数组  
        for (int i = 0; i < 5; i++) {  
            array[i] = i;  
        }  
      
        // 打印数组内容  
        printf("Original array:\n");  
        for (int i = 0; i < 5; i++) {  
            printf("%d ", array[i]);  
        }  
        printf("\n");  
      
        // 尝试访问超出数组末尾的索引(数组溢出)  
        // 注意:这是一个错误的行为,应该避免  
        // 在这里,我们尝试访问array[5],但是array只有从array[0]到array[4]的5个元素  
        printf("Trying to access out-of-bounds element: %d\n", array[5]);  // 数组溢出  
      
        // 释放内存  
        free(array);  
      
        return 0;  
    }

图4-2-3

(三)General (-check=alloc)、Intensive (-check=memory)这两种形式的内存检查都需要一个目标可遍历堆栈。如果启用内存检查,则在编译任何可能调用malloc(直接或间接)的函数时必须使能Build options中的Advanced->Debugging Options->Generate Target-Walkable Stack选项(如图4-3),这个选项会控制允许目标遍历其自身堆栈的代码的生成。请注意,当启用-check=alloc或-check=memory时,-gtws是默认值。因此,如果将其中一个传递给整个项目,-gtws将自动启用。

图4-3

五、GHS的Number of Callers to Track with Run-Time Memory Checks的使用

(一)Number of Callers to Track with Run-Time Memory Checks:是用于在启用运行时内存检查时,指定应该为每个分配跟踪和报告的调用者数量。这个选项需要搭配Run-Time Memory Checks这个选项进行使用。

(二)在配置上的话,一般建议value的值在5以上。默认的值是8个。并且显示的caller数量将限于MULTI所支持的数量。使用multi7,任何超过5的额外caller将不会显示。

六、讨论分析

  1. Number of Callers to Track with Run-Time Memory Checks选项中Callers的概念是什么?

答:这里的“caller”指的是在程序执行过程中调用某个函数或变量的上层调用者。换句话说,“caller”是指那些在程序中进行函数调用的代码位置或上下文。当启用运行时内存检查时,系统会跟踪并报告哪些代码(即哪些调用者)导致了内存分配。这有助于开发者了解内存分配的来源,从而更好地进行调试和优化内存使用。

  1. 解引用是什么?

答:解引用(dereferencing)是指在编程中通过指针访问指针所指向的内存地址上的数据或对象的过程。简单来说,就是通过指针找到它指向的具体值或对象。解引用是指针操作的核心,允许程序通过指针来访问和操作内存中的数据。正确地使用解引用可以有效地管理和操作复杂的数据结构,如链表、树和图等。但错误的解引用,尤其是空指针解引用,则会导致程序崩溃或未定义行为,因此在解引用前进行空指针检查是良好的编程实践。

  1. 为什么在switch语句中开关表达式与任何大小写标签不匹配,会产生内存问题?

答:在 switch 语句中,如果没有 default 标签,当开关表达式与任何 case 标签都不匹配时,程序会跳过 switch 语句内部的代码,继续执行 switch 语句后的代码。虽然这不会直接引发内存问题,但可能会导致程序的逻辑错误或未定义行为,这些错误可能会间接引发内存问题。

七、结论

通过GHS的Run-Time检查功能,可以有效地避免内存泄漏问题的产生。防止出现变量赋值越界、数据越界、空指针解引用、内存重复释放、数组溢出错误等会直接导致程序崩溃的内存问题的出现。在调试阶段和构建阶段就可以直接将问题点给指出来,不需要一个一个地调试确认,重复运行程序确认内存泄漏触发的位置。利用好GHS的Run-Time检查功能可以大大地提高工作效率,进一步地规范代码编写的标准,也提供了更优的内存问题的检查方式。

————————————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

关于亿道电子

亿道电子技术有限公司(www.emdoor.cn)是国内资深的研发工具软件提供商,公司成立于2002年,面向中国广大的制造业客户提供研发、设计、管理过程中使用的各种软件开发工具,致力于帮助客户提高研发管理效率、缩短产品设计周期,提升产品可靠性。

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

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

相关文章

华为开源自研AI框架昇思MindSpore应用案例:基于MindSpore框架的Swin Transformer

如果你对MindSpore感兴趣&#xff0c;可以关注昇思MindSpore社区 模型简介 模型背景 Swin Transfromer在2021年首次发表于论文《Swin Transformer: Hierarchical Vision Transformer using Shifted Windows》&#xff0c;目前已用于图像分割、分类等计算机视觉领域的各项任务…

如何搭建医疗陪诊平台?基于互联网医院系统源码的开发技术详解

本篇文章&#xff0c;小编将深入探讨医疗陪诊平台的搭建过程&#xff0c;尤其是基于互联网医院系统源码的开发技术&#xff0c;以期为有志于此领域的开发者提供参考。 一、选择合适的互联网医院系统源码 在搭建医疗陪诊平台时&#xff0c;选择合适的互联网医院系统源码至关重…

【前端开发入门】前端开发环境配置

目录 引言一、Vscode编辑器安装1. 软件下载2. 软件安装3. 插件安装 二、Nodejs环境安装及版本控制1. 安装内容2. 使用nvm安装2.1 软件下载并安装2.2 nvm基本指令2.3 nvm下载过慢导致超时解决 三、git安装及配置1. 软件下载2. 软件安装3. 基础配置 四、总结 引言 本系列教程旨在…

SRC漏洞挖掘 | 针对Spring-Boot 框架漏洞的初探

&#x1f497;想加内部圈子&#xff0c;请联系我&#xff01; &#x1f497;文章交流&#xff0c;请联系我&#xff01;&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 一个想当文人的黑客 &#xff0c;很高兴认识大家~ ✨主…

fmt:C++ 格式化库

fmt 是一个现代化、快速且安全的 C 格式化库&#xff0c;专注于高效地格式化文本。它提供了类似 Python 的 format 功能&#xff0c;但具有更高的性能和类型安全特性。fmt 库在处理字符串格式化、日志输出以及构建用户友好的输出时尤为强大。自从 C20 标准引入 std::format 后&…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《故障扰动下的风火储送端系统频率动态特性与储能容量优化配置》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

婚前协议模版(琴生生物机械科技工业研究所)

婚前协议示例 本协议由甲方&#xff08;全名&#xff09;&#xff0c;身份证号码&#xff08; _________ &#xff09;&#xff0c;与乙方&#xff08;全名&#xff09;&#xff0c;身份证号码&#xff08; _________ &#xff09;&#xff0c;在平等、自愿、相互尊重及充分沟…

DataEase v2 开源代码 Windows 从0到1环境搭建

一、环境准备 功能名称 描述 其它 操作系统 Windows 数据库 Mysql8.0 开发环境 JDK17以上 本项基于的21版本开发 Maven 3.9版本 开发工具 idea2024.2版本 前端 VSCode TIPS&#xff1a;如果你本地有jdk8版本&#xff0c;需要切换21版本&#xff0c;请看…

[python] 基于PyOD库实现数据异常检测

PyOD是一个全面且易于使用的Python库&#xff0c;专门用于检测多变量数据中的异常点或离群点。异常点是指那些与大多数数据点显著不同的数据&#xff0c;它们可能表示错误、噪声或潜在的有趣现象。无论是处理小规模项目还是大型数据集&#xff0c;PyOD提供了50多种算法以满足用…

解决银河麒麟fcitx进程资源占用高的问题

解决银河麒麟fcitx进程资源占用高的问题 1、问题描述2、解决方法 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 1、问题描述 银河麒麟桌面系统中&#xff0c;fcitx进程占用CPU和内存过高&#xff0c;导致系统卡顿。 2、解决方法 卸载并清…

OPENCV判断图像中目标物位置及多目标物聚类

文章目录 在最近的项目中&#xff0c;又碰到一个有意思的问题需要通过图像算法来解决。就是显微拍摄的到的医疗图像中&#xff0c;有时候目标物比较偏&#xff0c;也就是在图像的比较偏的位置&#xff0c;需要通过移动样本&#xff0c;将目标物置于视野正中央&#xff0c;然后再…

IP协议讲解

IP协议 IP协议的本质&#xff1a;提供一种能力&#xff0c;将数据跨网络从A主机传输到B主机 4位版本号(version): 指定IP协议的版本, 对于IPv4来说, 就是4. 4位头部长度(header length): IP头部的长度是多少个32bit, 也就是 length * 4 的字节数. 4bit表示最大 的数字是15, 因…

Linux(三)文件管理、复杂操作与实用工具详解

Linux学习笔记&#xff08;三&#xff09;文件管理、复杂操作与实用工具详解 Linux 学习笔记&#xff08;二&#xff09;&#xff1a;深入理解用户管理、运行级别与命令行操作 1.文件操作的基本操作 1.1 创建 创建目录 mkdir&#xff1a;创建目录 mkdir /home/dog # 创建单级…

【顺序表使用练习】发牌游戏

【顺序表使用练习】发牌游戏 1. 介绍游戏2. 实现52张牌3. 实现洗牌4. 实现发牌5. 效果展示 1. 介绍游戏 首先先为大家介绍一下设计要求 实现52张牌&#xff08;这里排除大小王&#xff09;洗牌——打乱牌的顺序发牌——3个人&#xff0c;1人5张牌 2. 实现52张牌 创建Code对象创…

NVIDIA G-Assist 项目:您的游戏和应用程序AI助手

NVIDIA G-Assist 是一个革命性的人工智能助手项目&#xff0c;旨在通过先进的AI技术提升玩家的游戏体验和系统性能。这个项目在2024年Computex上首次亮相&#xff0c;展示了其在游戏和应用程序中的潜在应用。 喜好儿网 G-Assist 的核心功能是提供上下文感知的帮助。它能够接收…

OLED移植

一、在D盘中找到OLED文件包 二、新建一个HAL库工程 只需要配好RCC和SYS以及时钟树就可以&#xff0c;不开启任何引脚 三、移植文件 把文件放在Core->Src里面 四、在Kile5中添加文件 五、注意 &#xff08;1&#xff09;下载的时候要开启Rsset and Run 不然下载不进程序 &a…

关于malloc,calloc,realloc

1.引用的头文件介绍&#xff1a; 这三个函数需要调用<stdlib.h>这个头文件 2.malloc 2.1 函数简单介绍&#xff1a; 首先这个函数是用于动态开辟一个空间&#xff0c;例如数组在c99标准之前是无法arr[N]的&#xff0c;这个时候就需要使用malloc去进行处理&#xff0c…

kettle从入门到精通 第八十八课 ETL之kettle kettle连接sqlserver彻底搞明白

场景&#xff1a;时不时群里面会有小伙伴咨询使用kettle连接ms sqlserver 数据库&#xff0c;折腾很久浪费时间&#xff0c;今天刚好有时间把这一块梳理下&#xff0c;希望能让大家节省时间提高效率。 1、首先要知道连接sqlserver 有两种方式&#xff0c;JTDS jdbc驱动和微软的…

Web安全 - 重放攻击(Replay Attack)

文章目录 OWASP 2023 TOP 10导图1. 概述2. 重放攻击的原理攻击步骤 3. 常见的重放攻击场景4. 防御重放攻击的技术措施4.1 使用时效性验证&#xff08;Time-Based Tokens&#xff09;4.2 单次令牌机制&#xff08;Nonce&#xff09;4.3 TLS/SSL 协议4.4 HMAC&#xff08;哈希消息…

4.1、FineReport单元格扩展和父子格

单元格扩展 1、配置数据集 2、纵向扩展 方法一&#xff1a; 方法二&#xff1a; 结果 多个字段纵向 2、横向扩展 方法一&#xff1a; 方法二&#xff1a; 结果 父子格 没什么特殊要求&#xff0c;就保持默认 1、右边的值默认以左边为左父格 2、下边的值默认以上边…