程序加载与运行过程中的资源分配与管理

news2024/9/20 22:26:04

目录

程序的加载

程序的内存空间

程序入口地址

BSS段初始化

程序运行过程中的堆栈管理

栈内存管理

变量的作用域:

栈溢出攻击原理

Linux堆内存管理

查看进程内存布局

内存分配器

内存块合并

top chunk


程序的运行分两种情况:一种是在有操作系统的环境下执行一个应用程序;另一种是在无操作系统的环境下执行一个裸机程序。

在Linux环境下,可执行文件是ELF格式,而在裸机环境下执行的程序一般是BIN/HEX格式。BIN/HEX文件是纯指令文件,没有其他的辅助信息。

虽然运行环境不同,文件格式也有所差异,但原理都是将指令加载到内存中的指定位置。而这个指定位置往往又与可执行文件链接时的链接地址有关。

程序的加载

拥有操作系统的计算机系统执行一个应用程序时,首先会运行一个叫作加载器的程序。加载器会根据软件的安装路径信息,将可执行文件从ROM中加载到内存,然后进行一些与初始化、动态库重定位相关的操作,最后才跳转到程序的入口运行。

程序的内存空间

程序是安装在磁盘上某个路径下的二进制文件,而进程则是一个程序运行的实例。一个进程实例不仅包括汇编指令代码、数据,还包括进程上下文环境、CPU寄存器状态、打开的文件描述符、信号、分配的物理内存等相关资源。

在Linux环境下运行的程序,在编译时链接的起始地址都是相同的,而且是一个虚拟地址。Linux操作系统需要CPU内存管理单元的支持才能运行,Linux内核通过页表和MMU硬件来管理内存,完成虚拟地址到物理地址的转换、内存读写权限管理等功能。每个进程都有各自的页表,用来记录各自进程中虚拟地址到物理地址的映射关系。

每一个应用程序进程都有4GB大小的虚拟地址空间。为了系统的安全稳定,0~4GB的虚拟地址空间一般分为两部分:用户空间和内核空间。0~3GB地址空间给应用程序使用,而操作系统一般运行在3~4GB内核空间。通过内存权限管理,应用程序没有权限访问内核空间,只能通过中断或系统调用来访问内核空间,这在一定程度上保障了操作系统核心代码的稳定运行。现在很多高端的SoC芯片,随着集成的IP模块越来越多,导致Linux内核镜像运行时需要的地址空间也越来越大。在很多处理器平台下,大家也经常看到如图5-3所示的划分:0~2GB的地址空间为用户空间,2~4GB的地址空间为内核空间。所有用户进程共享内核地址空间,但独享各自的用户地址空间。

程序入口地址

在Linux环境下运行的程序一般都会被封装成进程,参与操作系统的统一调度和运行。若用Shell终端环境运行一个程序,此时shell终端就充当加载器的角色,Shell终端程序一般会先fork一个子进程,创建一个独立的虚拟进程地址空间,接着调用execve函数将要运行的程序加载到进程空间:通过可执行文件的文件头,找到程序的入口地址,建立进程虚拟地址空间与可执行文件的映射关系,将PC指针设置为可执行文件的入口地址,即可启动运行。

可执行文件的文件头提供了文件类型、运行平台、程序的入口地址等基本信息,加载器在加载程序之前会首先根据文件头的信息做一些判断,如果发现程序的运行平台和当前的环境不符,则会报出错处理。除此之外,可执行文件中还有一个叫作段头表(program header table)的section,记录着如何将可执行文件加载到内存的相关信息,包括可执行文件中要加载到内存中的段、入口地址等信息。可重定位目标文件因为是不可执行的,不需要加载到内存中,所以段头表这个section在目标文件中不是必须存在的,是可选的。可以使用readelf命令查看可执行文件的段头表。

arm-linux-gnueabi-readelf -l a.out

程序的入口地址可通过下面的计算公式得到:
程序的入口地址=编译时的链接地址+一定偏移(程序头等会占用一部分空间)

编译器在编译一个工程时,默认的程序入口是_start符号,而不是main。符号main是一个约定符号,它用来告诉编译器在一个项目中哪里是程序的入口点。程序员在开发一个项目时,也会遵守这个约定,使用main()函数作为项目的入口函数。

在main()函数运行之前,已经有“先头部队”代码提前运行了:它们主要完成运行main()函数之前的一些初始化工作:

  • C语言运行的基本堆栈环境、进程环境
  • 初始化data段的内容,初始化static静态变量和global全局变量,并给BSS段的变量赋初值
  • 动态库的加载、释放、初始化、清理等工作
  • 将用户传入的参数传递给main

最后才跳入main()函数运行

这部分初始化代码是在程序编译阶段,由编译器自动添加到可执行文件中的。这部分代码属于C运行库(C Running Time,CRT)中的代码,编译器厂商在开发编译器时,除了实现C语言标准中规定的printf、fopen、fread等标准函数,还会实现这部分初始化代码。在ARM交叉编译器安装路径下的lib目录下,你会看到一个叫作crt1.o的目标文件,这个文件其实就是由汇编初始化代码编译生成的,是CRT的一部分。在链接过程中,链接器会将crt1.o这个目标文件和项目中的目标文件组装在一起,生成最终的可执行文件。

main只是编译器和程序员约定好的默认入口点,并不是一成不变的,程序员也可以自定义程序入口。可改变项目的入口地址

#arm-linux-gnueabi-gcc -nostartfiles -e <入口函数> xx.c

BSS段初始化

对于未初始化的全局变量和静态局部变量,编译器将其放置在BSS段中。BSS段是不占用可执行文件存储空间的,但是当程序加载到内存运行时,加载器会在内存中给BSS段开辟一段存储空间。在section header table中会记录BSS段的大小,在符号表中会记录每个变量的地址和大小。加载器会根据这些信息,在数据段的后面分配指定大小的内存空间并清零,根据符号表中各个变量的地址,在这片内存中给各个未初始化的全局变量、静态变量分配存储空间。int类型的全部初始化为0,布尔型的变量初始化为FALSE,指针型的变量初始化为NULL

程序运行过程中的堆栈管理

在一个进程的地址空间中,代码段、数据段、BSS段在程序加载运行后,地址就已经固定了,在整个程序运行期间不再发生变化,这部分内存一般也称为静态内存。而在程序中使用malloc申请的内存、函数调用过程中的栈在程序运行期间则是不断变化的,这部分内存一般也称为动态内存。用户使用malloc申请的内存一般被称为堆内存(heap),函数调用过程中使用的内存一般被称为栈内存(stack)。

栈内存管理

栈的初始化其实就是栈指针SP的初始化。在系统启动过程中,内存初始化后,将栈指针指向内存中的一段空间,就完成了栈的初始化,栈指针指向的这片内存空间被称为栈空间。不同的处理器一般都会使用专门的寄存器来保存栈的起始地址,X86处理器一般使用ESP(栈顶指针)和EBP(栈底指针)来管理堆栈,而ARM处理器则使用R13寄存器(SP)和R11寄存器(FP)来管理堆栈。

ARM处理器使用的是满递减栈,在Linux环境下,栈的起始地址一般就是进程用户空间的最高地址,紧挨着内核空间,栈指针从高地址往低地址增长。为了防止黑客栈溢出攻击,新版本的Linux内核一般会将栈的起始地址设置成随机的,如图5-9所示,每次程序运行,栈的初始化起始地址都会基于用户空间的最高地址有一个随机的偏移,每次栈的起始地址都不一样。

栈初始化后,栈指针就指向了这片栈空间的栈顶,当需要入栈、出栈操作时,栈指针SP就会随着栈顶的变化上下移动。在一个满递减栈中,栈指针SP总是指向栈顶元素。

在栈的初始化过程中,除了指定栈的起始地址,我们还需要指定栈空间的大小。在Linux环境下,我们可以通过下面的命令来查看和设置栈的大小。

#ulimit -s  //查看栈大小 单位 KB
#ulimit -s 4096  //设置栈空间大小  

Linux默认给每一个用户进程栈分配8MB大小的空间。栈的容量如果设置得过大,则会增加内存开销和启动时间;如果设置得过小,则程序超出栈设置的内存空间又容易发生栈溢出(Stack Overflow),产生段错误(Segmentation fault (core dumped))。

一个函数内定义的局部变量、传递的实参都是保存在栈中的。每一个函数都会有自己专门的栈空间来保存这些数据,每个函数的栈空间都被称为栈帧(Frame Pointer,FP)。每一个栈帧都使用两个寄存器FP和SP来维护,FP指向栈帧的底部,SP指向栈帧的顶部。无论函数调用运行到哪一级,SP总是指向当前正在运行函数栈帧的栈顶,而FP总是指向当前运行函数的栈底。在每一个函数栈帧中,除了要保存局部变量、函数实参、函数调用者的返回地址,有时候编译过程中的一些临时变量也会保存到函数的栈帧中,为了简化分析,我们暂不考虑这些。除此之外,上一级函数栈帧的起始地址,即栈底也会保存到当前函数的栈帧中,多个栈帧通过FP构成一个链,这个链就是某个进程的函数调用栈。很多调试器支持回溯功能,其实就是基于这个调用链来分析函数的调用关系的。

示例:

局部变量:

根据fp的偏移来压入局部变量

参数传递:

根据sp的偏移压入函数参数

函数调用过程中的参数传递,一般都是通过栈来完成的。ARM处理器为了提高程序运行效率,会使用寄存器来传参。根据ATPCS规则,在函数调用过程中,当要传递的参数个数小于4时,直接使用R0~R3寄存器传递即可;当要传递的参数个数大于4时,前4个参数使用寄存器传递,剩余的参数则压入堆栈保存。

 在参数传递过程中,各个参数压栈、出栈的顺序也要有一个约定,是从左往右依次压入堆栈的呢?还是从右往左呢?我们一般把不同的约定方式称为调用惯例。

语言默认使用cdecl调用惯例。参数传递时按照从右到左的顺序依次压入堆栈,栈的清理方则由函数调用者caller管理。使用cdecl调用惯例的好处是可以预先知道参数和返回值大小,而且可以支持变参函数的调用。

对应的汇编

变量的作用域:

全局变量定义在函数体外,全局变量的作用域如下。

● 全局变量的作用域由文件来限定。

● 可使用extern进行扩展,被其他文件引用。

● 也可以使用static进行限制,只能在本文件中被引用。

局部变量定义在函数内,局部变量的作用域如下。

● 局部变量的作用域由{}限定。

● 可以使用static修饰局部变量来改变它们的存储属性(生命周期),但不能改变其作用域。

栈溢出攻击原理

在一个函数的栈帧中一般都会保存上一级函数的返回地址LR,当函数运行结束时就会根据这个返回地址跳到上一级函数继续执行。黑客如果发现你实现的某个函数有漏洞,就可以利用漏洞修改栈的返回地址LR,植入自己的指令代码。

 

 虽然C语言标准并没有规定数组的越界访问会报错,但是大多数编译器为了安全考虑,会对数组的边界进行自行检查:当发现数组越界访问时,会产生一个错误信息来提醒开发者。

Linux堆内存管理

堆内存管理,不同的嵌入式开发环境,不同的操作系统实现也不完全相同

动态申请/释放的内存就属于堆内存,跟内存申请相关的函数:

#include <stdlib.h>

void *malloc(size_t size); //堆内存空间中申请一块用户指定大小的内存
void free(void *ptr);
void *calloc(size_t nmemb, size_t size); //在堆内存中申请nmemb个单位长度为size的连续空间,并将这块内存初始化为0
void *realloc(void *ptr, size_t size);  //当申请的内存不够用时,我们可以使用realloc()函数动态调整内存块的大小,realloc()函数会新申请一块大小为200字节的空间,并将原来内存上的数据复制过来,返回给用户新申请空间的指针。

malloc()/free()函数的底层实现,其实就是通过系统调用brk向内核的内存管理系统申请内存。内核批准后,就会在BSS段的后面留出一片内存空间,允许用户进行读写操作。申请的内存使用完毕后要通过free()函数释放,free()函数的底层实现也是通过系统调用来归还这块内存的。

当用户要申请的内存比较大时,如大于128KB,一般会通过mmap系统调用直接映射一片内存,使用结束后再通过ummap系统调用归还这块内存。mmap区域是Linux进程中比较特殊的一块区域,主要用于程序运行时动态共享库的加载和mmap文件映射。早期的Linux内核将该区域设置在0x40000000附近,Linux 2.6以后的内核将该区域移到了栈附近,打印mmap映射区域的地址,你会发现大部分地址都在0xBxxxxxxx范围内,紧挨着进程的用户栈。

查看进程内存布局

# ps |grep xxx  找进程id
# cat /proc/pid/maps

 栈的起始地址并不紧挨着内核空间0xc0000000,而是从0xbf9a2000作为起始地址,中间有一个大约6MB的偏移。heap区也不紧挨着.bss段,它们之间也有一个offset;mmap区也是如此,它和stack区之间也有一个offset。这些随机偏移由内核支持的可选配置选项/proc/sys/kernel/randomized_va_space控制,也可以关闭这个功能。将randomize_va_space赋值为0,可以关掉这个随机偏移功能。关闭这个功能后再去运行a.out,进程栈的起始地址就紧挨着内核空间0xc0000000存放,heap区和mmap区也是。

 当程序加载到内存运行时,加载器会根据可执行文件的代码段、数据段(.data和.bss)的size大小在内存中开辟同等大小的地址空间。代码段和数据段的大小在编译时就已经确定,代码段具有只读和执行的权限,而数据段则有读写的权限。代码段和栈之间的一片茫茫内存虽然都是空闲的,但是要先申请才能使用。brk()系统调用通过扩展数据段的终止边界来扩大进程中可读写内存的空间,并把扩展的这部分内存作为堆区,使用start_brk和brk来标注堆区的起始和终止地址。在程序运行期间,随着用户申请的动态内存不断变化,brk的终止地址也随之不断地变化

内存分配器

大量的系统调用会让处理器和操作系统在不同的工作模式之间来回切换:操作系统要在用户态和内核态之间来回切换,CPU要在普通模式和特权模式之间来回切换,每一次切换都意味着各种上下文环境的保存和恢复,频繁地系统调用会降低系统的性能。系统调用还有一个不人性化的地方是不支持任意大小的内存分配,有的平台甚至只支持一个或数倍物理页大小的内存申请,这在一定程度上会造成内存的浪费。为了提高内存申请效率,减少系统调用带来的开销,在用户空间层面对堆内存介入管理。如在glibc中实现的内存分配器(allocator)可以直接对堆内存进行维护和管理。

内存分配器通过系统调用brk()/mmap()向Linux内存管理子系统“批发”内存,同时实现了malloc()/free()等API函数给用户使用,满足用户动态内存的申请与释放请求。当用户使用free()释放内存时,释放的内存并不会立即返回给内核,而是被内存分配器接收,缓存在用户空间。内存分配器将这些内存块通过链表收集起来,等下次有用户再去申请内存时,可以直接从链表上查找合适大小的内存块给用户使用,如果缓存的内存不够用再通过brk()系统调用去内核“批发”内存。内存分配器相当于一个内存池缓存,通过这种操作方式,大大减少了系统调用的次数,从而提升了程序申请内存的效率,提高了系统的整体性能。

Linux环境下的C标准库glibc使用ptmalloc/ptmalloc2作为默认的内存分配器,具体的实现源码在glibc-2.xx/malloc目录下。为了方便对内存块进行跟踪和管理,对于每一个用户申请的内存块,ptmalloc都使用一个malloc_chunk结构体来表示,每一个内存块被称为chunk。

用户程序调用free()释放掉的内存块并不会立即归还给操作系统,而是被用户空间的ptmalloc接收并添加到一个空闲链表中。malloc_chunk结构体中的fd和bk指针成员将每个内存块链成一个双链表,不同大小的内存块链接在不同的链表上,每个链表都被我们称作bin,ptmalloc内存分配器共有128个bin,使用一个数组来保存这些bin的起始地址。每一个bin都是由不同大小的内存块链接而成的链表,根据内存块大小的不同,我们可以对这些bins进行分类:

unsorted bin: 用户释放掉的内存块不会立即放到bins中,而是先放到unsorted bin中

small bins:内存数据块的大小范围为[16,504]

large bins:内存数据块的大于504字节

每个bin在数组中的索引和内存块大小之间的关系如表5-2所示。

除了数组中的这些bins,还有一些特殊的bins,如:

fast bins:用户释放掉的小于M_MXFAST(32位系统下默认是64字节)的内存块会首先被放到fast bins中。fast bins由单链表构成,FILO栈式操作,运行效率高,相当于small bins的缓存。

当用户申请一块内存时:

  • 如果申请的内存块小于M_MXFAST时,ptmalloc分配器会首先到fast bins中去看看有没有合适的内存块,如果没有找到,则再到small bins中查找。
  • 如果申请的内存块大于512字节,则到unsorted bin中查找,再到lar个bins中查找
  • 如果在large bins中还没有找到合适的内存块,这时候就要到top chunk上去分配内存了。

内存块合并

在适当的时机,fast bins会将物理相邻的空闲内存块合并,存放到unsorted bin中。内存分配器如果在unsorted bin中没有找到合适大小的内存块,则会将unsorted bins中物理相邻的内存块合并,根据合并后的内存块大小再迁移到small bins或large bins中。

     

top chunk

top chunk是堆内存区顶部的一个独立chunk,它比较特殊,不属于任何bins。若用户申请的内存小于top chunk,则top chunk会被分割成两部分:一部分返回给用户使用,剩余部分则作为新的top chunk。若用户申请的内存大于top chunk,则内存分配器会通过系统调用sbrk()/mmap()扩展top chunk的大小。用户第一次调用malloc()申请内存时,ptmalloc会申请一块比较大的内存,切割一部分给用户使用,剩下部分作为top chunk。当用户申请的内存大于M_MMAP_THRESHOLD(默认128KB)时,内存分配器会通过系统调用mmap()申请内存。使用mmap映射的内存区域是一种特殊的chunk,这种chunk叫作mmap chunk。当用户通过free()函数释放掉这块内存时,内存分配器再通过munmap()系统调用将其归还给操作系统,而不是将其放到bin中。

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

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

相关文章

矩阵理论复习(九)

A为正规矩阵时&#xff0c;A的奇异值是A的特征值的模。A为半正定Hermite矩阵时&#xff0c;A的奇异值是A的特征值。 最佳逼近解 最小二乘解 矩阵的单边逆 A是左可逆的充要条件是A为列满秩矩阵 A是左可逆的充要条件是NA{0} 投影矩阵N(A)R(I-A),N(I-A)R(A) A是右可逆的充要…

腾讯电子签小程序跳转(app 跳小程序,小程序跳小程序) Api

腾讯电子签 官网地址&#xff1a;腾讯电子签跳转 api 文档 let id‘yDw9jUUgyg34gq97U7WZ9b1rWEBV******’ let name ‘张**’ let phone ‘MTQ3NDU3Oidioidkl’ let path pages/guide?fromSFY&toMVP_CONTRACT_COVER&id${id}&name${name}&phone${phone} wx…

Echarts解决左右上下边距问题( 两种方法)

第007个点击查看专栏目录文章目录示例效果示例源代码&#xff08;共88行&#xff09;相关资料参考相应的设置参数&#xff08;方法1&#xff09;相应的设置参数&#xff08;方法2&#xff09;专栏介绍示例效果 没有添加grid之前&#xff08;有grid的默认值来控制&#xff09; …

Web 3 财富分配方式

文章作者&#xff1a;Andrew Beal每个星期四&#xff0c;Forta 基金会团队都会在 Zoom 上聚会&#xff0c;享受虚拟欢乐时光。我还没有亲自见过我的一些同事&#xff0c;所以这是我唯一一次了解他们在办公室之外的身份。每个人都有故事要讲&#xff0c;你只需要问。规则是 “我…

Canal安装和配置

Canal安装和配置1.开启MySQL主从1.1.开启binlog1.2.设置用户权限2.安装Canal2.1.创建网络2.2.安装Canal2.3.查看canal是否与mysql建立连接下面我们就开启mysql的主从同步机制&#xff0c;让Canal来模拟salve 1.开启MySQL主从 Canal是基于MySQL的主从同步功能&#xff0c;因此…

wamp内置mysql和学习后端下载mysql相冲突问题

文章目录前言1.将后端的mysql放入wamp路径下2.打包代码文件3&#xff0c;查询wamp集合环境换mysql的办法成功way前言 尝试了各种能够兼容两者的办法都失败了 所以一气之下把wamp内的mysql删了&#xff0c;使前后端都使用一个mysql 1.将后端的mysql放入wamp路径下 将后端的mysq…

【6s965-fall2022】量化 Quantization Ⅱ

什么是线性量化 rS(q−Z)r S(q - Z)rS(q−Z) 式中&#xff0c;SSS是比例因子&#xff0c;通常是一个浮点数&#xff1b;qqq是rrr的量化后的表示&#xff0c;是一个整数&#xff1b;ZZZ也是一个整数&#xff0c;把qqq中和ZZZ相同的整数映射到rrr中零&#xff0c;因此ZZZ是零点偏…

24_mimikatz

mimikatz 一、介绍 二、修改注册表抓取明文密码 当目标为win10或2012R2以上时&#xff0c;默认在内存缓存中禁止保存明文密码&#xff0c;但可以通过修改注册表的方式抓取明文。 重启或用户重新登录后可以成功抓取 reg add HKLM\SYSTEM\CurrentControlSet\Control\Security…

Java设计模式总结

java常用七种设计模式总结单例模式单例模式的实现第 1 种&#xff1a;懒汉式单例第 2 种&#xff1a;饿汉式单例工厂方法模式一&#xff0c;简单工厂模式二、工厂方法模式三、抽象工厂模式建造者模式策略模式模板方法责任链模式代理模式适配器模式观察者模式单例模式 单例模式…

第三届腾讯Light技术公益创造营启动

简介 腾讯Light技术公益创造营通过持续探索技术与公益的结合&#xff0c;已经打造出了包括推进公众参与中华白海豚保育的公益计划、使用AI技术助力新生儿黄疸诊断的“高危儿拯救计划”等一批优秀项目 全面升级的第三届腾讯Light技术公益创造营在海南海口正式启动&#xff0c;…

Redis 分布式锁实现详解

一、概述分布式锁&#xff0c;即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题&#xff0c;而分布式锁&#xff0c;就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是&#xff0c;分布式系统中竞争共享资源的最小粒度从线程升级成…

Go练手==若依go语言版本开发day01

代码地址&#xff1a;ry-vue-go: 使用GO开发若依后台 启动方式&#xff1a; pycharm打开项目后点击main.go启动后端 打开文件夹浏览器后&#xff0c;点击ruoyi-ui下的z_start_ui.bat启动前端 数据库 mysql 缓存redis ORM框架 GORM WEB框架 GIN 今日实现功能 mysql连接池U…

python采集《狂飙》评论,看看是什么让它如此火爆

前言 大家早好、午好、晚好吖 ❤ ~ “是非面前稍不留神&#xff0c;就会步入万丈深渊&#xff0c;唯有坚守信仰&#xff0c;才能守得初心” 2023年首部爆款剧集《狂飙》迎来大结局&#xff0c;今天我们就来采集一下评论,看看为什么它这么火爆 开发环境: python 3.8 pycharm …

计算机网络-TCP如何保证传输可靠性

TCP协议传输的特点主要就是面向字节流、传输可靠、面向连接。 TCP协议如何确保传输的可靠性的? 确保传输可靠性的方式 TCP协议保证数据传输可靠性的方式主要有&#xff1a; 1.校验和 2.序列号 3.确认应答 4.超时重传 5.连接管理 6.流量控制 7.拥塞控制 1.校验和 发送方&a…

MVC,MVP和MVVM框架之间的理解

一、MVC的理解 1、MVC是什么 MVC, 即Model-View-Controller, 基于页面逻辑的修改要多于业务逻辑, 分离两种逻辑减少类代码的修改 Model: 即数据层, 负责处理业务逻辑, 监听网络与数据库接口View: 即界面(UI)层, 显示来源于Model的数据Contoller: 即逻辑层, 传递用户的交互和更…

面向对象三大特征之三:多态、内部类、常用API

目录 面向对象三大特征之三&#xff1a;多态 多态的概述、形式 多态的优势 多态下引用数据类型的类型转换 多态的案例 内部类 内部类的概述 内部类之一&#xff1a;静态内部类[了解] 内部类之二&#xff1a;成员内部类[了解] 内部类之三&#xff1a;局部内部类[了解]…

智能DTU

什么是DTU百度百科&#xff1a;DTU (Data Transfer unit)&#xff0c;是专门用于将串口数据转换为 IP 数据或将 IP 数据转换为串口数据通过无线通信网络进行传送的无线终端设备。DTU目前现状在物联网大爆发的时代&#xff0c;除了各种传感器&#xff0c;DTU 设备也得到了很大的…

免费舆情监控工具有哪些,TOOM加强舆情监控提高应对能力

免费舆情监控是指通过免费的网络舆情监控工具&#xff0c;对网络上关于话题、公司、产品或者人物的舆论进行监测和分析&#xff0c;旨在帮助用户了解社会舆论的变化和趋势。免费舆情监控工具一般提供基本的舆论监测功能&#xff0c;功能较为有限&#xff0c;但是对于个人用户或…

JavaWeb-HTML入门

目录一、认识HTML二、学习HTML语法2.1 注释标签2.2 标题标签2.3 段落标签 p2.3 换行标签2.4 字体设置标签2.5 图片标签2.6 超链接标签2.7 表格标签2.8 列表标签2.9 表单标签2.10 select下拉菜单2.11 textarea多行编辑框2.12 无语义标签一、认识HTML HTML的全称为超文本标记语言…

车载网络 - Autosar网络管理 - 处理逻辑时间参数

前面已经将Autosar常用的缩写名词、运行状态机、个状态报文发送状态等基础信息进行了说明,下面就剩下时间参数和网络管理逻辑,如果要讲处理的逻辑的话我个人觉得还是先要把时间参数介绍下,毕竟时间参数贯穿于整个处理逻辑中。 时间参数: 作为衡量一个软件产品性能的重要指标…