当前位置: 首页 > news >正文

ASM 字节码插桩:隐私合规方法检测!

1.前言

近两年来工信部对于应用的隐私合规安全问题愈加重视,对 Android 平台的管控程度也要比 IOS 平台严格很多,很多不合规的应用也先后被下架要求整改。笔者就曾遇到过加班整改隐私合规的问题,隐私合规问题主要针对两个方面。

  • 在用户同意隐私协议之前不能收集用户隐私数据,例如 IMEI、AndroidId、MAC 等

  • 在用户同意隐私协议之后,收集用户数据行为在对应场景不能超频。比如一分钟不能超过 3 次获取 IMEI

针对上述两个方面,有以下措施来针对

  • 通过静态扫描,收集项目中(自有代码 + 三方 sdk)使用隐私合规相关 api 的相关代码

  • 通过 ASM 插桩,在调用隐私合规 api 之前插入代码,记录运行时的方法调用链和当前时间

  • hook 隐私合规 api,替换字节码指令将调用链指向工具类,在未同意隐私协议之前,不调用相关的 api

2.实现

2.1 注解和工具类

通过定义注解和工具类,用来定义要处理哪些隐私合规相关的方法,目前笔者已经处理了大部分,请放心食用

2.1.1 注解
/*** 收集和注解匹配的方法* visitMethodInsn(int opcode, String owner, String name,String desc)** ======如果 originName 和 originDesc 传"",逻辑会在插件中处理=====*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD})
public @interface AsmMethodReplace {/*** 指令操作码*/int targetMethodOpcode();/*** 方法所有者类*/String targetClass();/*** 方法名称*/String targetName() default "";/*** 方法描述符*/String targetDesc() default "";/*** 是否进行 hook*/boolean hook() default false;}

该注解用来匹配调用隐私合规 api 的字节码指令,例如通过 ASM 调用 getImei 的字节码指令为methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "android/telephony/TelephonyManager", "getImei", "()Ljava/lang/String;", false);targetName 属性和 targetDesc 可以不赋值,这里做了取巧的处理,会根据工具类的 method name 和method descriptor 推断出调用隐私合规方法的字节码指令,这块后面插件会处理。最后的 hook 属性表示是否 hook 掉原始的调用链,将调用指向工具类中的方法。

2.1.2 工具类

上述注解可以用在任何地方,笔者将常用的隐私合规的方法聚合起来,方法工具类中统一处理,例如处理 IMEI 的逻辑如下:

@RequiresApi(api = Build.VERSION_CODES.O)
@AsmMethodReplace(targetMethodOpcode = OPCODE_INVOKEVIRTUAL, targetClass = CLASS_NAME_TELEPHONYMANAGER,hook = true)
public static String getImei(TelephonyManager telephonyManager) {if (!checkAgreePrivacy("getImei")) {Log.e(TAG, TIP);return "";}if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {Log.i(TAG, "getImei-SDK_INT above android Q");return "";}return telephonyManager.getImei();
}

如果还没有同意隐私协议,直接 return “”,否者走正常的调用方法。同时,通过工具类

   @Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);ConfigGlobal.getInstance().setStoreDirectory(base.getExternalCacheDir().getAbsolutePath());}

设置存储调用堆栈和时间的文件路径。

2.2 插件处理

gradle 插件的基本使用在这里就不赘述了,这里主要是两方面的处理

  • 编译时扫描代码,处理有定义特定注解的方法,分析字节码指令,收集所有需要处理的隐私合规相关 api 相关的信息

  • 再次扫描,根据第一次扫描收集到的信息,判断当前类是否含有调用隐私合规 api 的字节码指令,如果有,在该类中注入一个写文件方法及在隐私合规 api 调用指令之前插入写文件的字节码指令,用来记录调用堆栈和频次。

2.2.1 模版代码

下面这块代码是我们在自定义 gradle 插件时常用的模版代码,供大家使用

abstract class BaseTransform extends Transform {AbstractExecutorService executorService = ForkJoinPool.commonPool()private List<Callable<Void>> taskList = new ArrayList<>()protected Project projectBaseTransform(Project project) {this.project = project}@OverrideString getName() {return getClass().simpleName}@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return true}@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation)println("transform start--------------->")if (firstTransform()) {printCopyRight()}onTransformStart(transformInvocation)def startTime = System.currentTimeMillis()def inputs = transformInvocation.inputsdef outputProvider = transformInvocation.outputProviderdef context = transformInvocation.contextdef isIncremental = transformInvocation.isIncremental()if (!isIncremental) {outputProvider.deleteAll()}//1
//        inputs.each { input ->
//            input.jarInputs.each { JarInput jarInput ->
//                forEachJar(jarInput, outputProvider, context, isIncremental)
//            }
//
//            input.directoryInputs.each { DirectoryInput dirInput ->
//                forEachDir(dirInput, outputProvider, context, isIncremental)
//            }
//        }//3inputs.each { input ->input.jarInputs.each { jarInput ->submitTask(new Runnable() {@Overridevoid run() {forEachJar(jarInput, outputProvider, context, isIncremental)}})}input.directoryInputs.each { DirectoryInput dirInput ->submitTask(new Runnable() {@Overridevoid run() {forEachDir(dirInput, outputProvider, context, isIncremental)}})}}def futures = executorService.invokeAll(taskList)futures.each { it ->it.get()}onTransformEnd(transformInvocation)println(getName() + "transform end--------------->" + "duration : " + (System.currentTimeMillis() - startTime) + " ms")}void submitTask(Runnable runnable) {taskList.add(new Callable<Void>() {@OverrideVoid call() throws Exception {runnable.run()return null}})}void forEachDir(DirectoryInput directoryInput, TransformOutputProvider outputProvider, Context context, boolean isIncremental) {def inputDir = directoryInput.fileFile dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)println "directoryInputPath:" + directoryInput.file.absolutePathprintln "destPath:" + dest.absolutePathdef srcDirPath = inputDir.absolutePathdef destDirPath = dest.absolutePathdef temporaryDir = context.temporaryDirFileUtils.forceMkdir(dest)Logger.info("srcDirPath:${srcDirPath}, destDirPath:${destDirPath}")if (isIncremental) {directoryInput.getChangedFiles().each { entry ->def classFile = entry.keyswitch (entry.value) {case Status.NOTCHANGED:Logger.info("处理 class: " + classFile.absoluteFile + " NOTCHANGED")breakcase Status.REMOVED:Logger.info("处理 class: " + classFile.absoluteFile + " REMOVED")//最终文件应该存放的路径def destFilePath = classFile.absolutePath.replace(srcDirPath, destDirPath)def destFile = File(destFilePath)if (destFile.exists()) {destFile.delete()}breakcase Status.ADDED:case Status.CHANGED:Logger.info("处理 class: " + classFile.absoluteFile + " ADDED or CHANGED")modifyClassFile(classFile, srcDirPath, destDirPath, temporaryDir)breakdefault:break}}} else {com.android.utils.FileUtils.getAllFiles(inputDir).each { File file ->modifyClassFile(file, srcDirPath, destDirPath, temporaryDir)}}}void modifyClassFile(classFile, srcDirPath, destDirPath, temporaryDir) {Logger.info("处理 class: " + classFile.absoluteFile)//目标路径def destFilePath = classFile.absolutePath.replace(srcDirPath, destDirPath)def destFile = new File(destFilePath)if (destFile.exists()) {destFile.delete()}Logger.info("处理 class:destFile" + destFile.absoluteFile)String className = CommonUtil.path2ClassName(classFile.absolutePath.replace(srcDirPath + File.separator, ""))Logger.info("处理 className: " + className)File modifyFile = nullif (CommonUtil.isLegalClass(classFile) && shouldHookClass(className)) {modifyFile = getModifyFile(classFile, temporaryDir, className)}if (modifyFile == null) {modifyFile = classFile}FileUtils.copyFile(modifyFile, destFile)}File getModifyFile(File classFile, File temporaryDir, String className) {byte[] sourceBytes = IOUtils.toByteArray(new FileInputStream(classFile))def tempFile = new File(temporaryDir, CommonUtil.generateClassFileName(classFile))if (tempFile.exists()) {FileUtils.forceDelete(tempFile)}def modifyBytes = modifyClass(className, sourceBytes)if (modifyBytes == null) {modifyBytes = sourceBytes}tempFile.createNewFile()def fos = new FileOutputStream(tempFile)fos.write(modifyBytes)fos.flush()IOUtils.closeQuietly(fos)return tempFile}void forEachJar(JarInput jarInput, TransformOutputProvider outputProvider, Context context, boolean isIncremental) {Logger.info("jarInput:" + jarInput.file)File destFile = outputProvider.getContentLocation(//防止同名被覆盖CommonUtil.generateJarFileName(jarInput.file), jarInput.contentTypes, jarInput.scopes, Format.JAR)//增量编译处理if (isIncremental) {Status status = jarInput.statusswitch (status) {case Status.NOTCHANGED:Logger.info("处理 jar: " + jarInput.file.absoluteFile + " NotChanged")//Do nothingreturncase Status.REMOVED:Logger.info("处理 jar: " + jarInput.file.absoluteFile + " REMOVED")if (destFile.exists()) {FileUtils.forceDelete(destFile)}returncase Status.ADDED:case Status.CHANGED:Logger.info("处理 jar: " + jarInput.file.absoluteFile + " ADDED or CHANGED")break}}if (destFile.exists()) {FileUtils.forceDelete(destFile)}CommonUtil.isLegalJar(jarInput.file) ? transformJar(jarInput.file, context.getTemporaryDir(), destFile): FileUtils.copyFile(jarInput.file, destFile)}def transformJar(File jarFile, File temporaryDir, File destFile) {Logger.info("处理 jar: " + jarFile.absoluteFile)File tempOutputJarFile = new File(temporaryDir, CommonUtil.generateJarFileName(jarFile))if (tempOutputJarFile.exists()) {FileUtils.forceDelete(tempOutputJarFile)}JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempOutputJarFile))JarFile inputJarFile = new JarFile(jarFile, false)try {def entries = inputJarFile.entries()while (entries.hasMoreElements()) {def jarEntry = entries.nextElement()def entryName = jarEntry.getName()def inputStream = inputJarFile.getInputStream(jarEntry)try {byte[] sourceByteArray = IOUtils.toByteArray(inputStream)def modifiedByteArray = nullif (!jarEntry.isDirectory() && CommonUtil.isLegalClass(entryName)) {String className = CommonUtil.path2ClassName(entryName)if (shouldHookClass(className)) {modifiedByteArray = modifyClass(className, sourceByteArray)}}if (modifiedByteArray == null) {modifiedByteArray = sourceByteArray}jarOutputStream.putNextEntry(new JarEntry(entryName))jarOutputStream.write(modifiedByteArray)jarOutputStream.closeEntry()} finally {IOUtils.closeQuietly(inputStream)}}} finally {jarOutputStream.flush()IOUtils.closeQuietly(jarOutputStream)IOUtils.closeQuietly(inputJarFile)}FileUtils.copyFile(tempOutputJarFile, destFile)}private byte[] modifyClass(String className, byte[] sourceBytes) {byte[] classBytesCodetry {classBytesCode = hookClassInner(className, sourceBytes)} catch (Throwable e) {e.printStackTrace()classBytesCode = nullprintln "throw exception when modify class ${className}"}return classBytesCode}/*** 打印日志信息*/static void printCopyRight() {println()println '#######################################################################'println '##########                                                    'println '##########                欢迎使用隐私合规处理插件'println '##########                                                    'println '#######################################################################'println '##########                                                    'println '##########                 插件配置参数                         'println '##########                                                    'println '##########                -isDebug: ' + PrivacyGlobalConfig.isDebugprintln '##########                -handleAnnotationName: ' + PrivacyGlobalConfig.handleAnnotationNameprintln '##########                -exclude: ' + PrivacyGlobalConfig.excludeprintln '##########                                                    'println '##########                                                    'println '##########                                                    'println '#######################################################################'println()}protected boolean firstTransform() {return false}boolean shouldHookClass(String className) {def excludes = PrivacyGlobalConfig.excludeif (excludes != null) {for (String string : excludes) {if (className.startsWith(string)) {return false}}}return shouldHookClassInner(className)}protected abstract boolean shouldHookClassInner(String className)protected abstract byte[] hookClassInner(String className, byte[] bytes)protected abstract void onTransformStart(TransformInvocation transformInvocation)protected abstract void onTransformEnd(TransformInvocation transformInvocation)
}
2.2.2 注解处理 transform

收集和处理具有特定注解的字节码指令,给下一个 transform 使用

@Override
byte[] hookClassInner(String className, byte[] bytes) {ClassReader cr = new ClassReader(bytes)ClassNode classNode = new ClassNode()cr.accept(classNode, 0)classNode.methods.each { methodNode ->//编译期注解methodNode.invisibleAnnotations.each { annotationNode ->if (PrivacyGlobalConfig.getHandleAnnotationName() == annotationNode.desc) {collectPrivacyMethod(annotationNode, methodNode, cr.className)}}}return bytes
}/*** 收集注解和注解关联的方法* @param annotationNode 注解信息* @param methodNode 方法信息*/
static collectPrivacyMethod(AnnotationNode annotationNode, MethodNode methodNode, String className) {List<Object> values = annotationNode.valuesLogger.info("annotation values : ${values}")MethodReplaceItem item = new MethodReplaceItem(values, methodNode, CommonUtil.getClassInternalName(className))PrivacyGlobalConfig.methodReplaceItemList.offer(item)Logger.info("collectPrivacyMethod success: ${item}")println("collectPrivacyMethod success: ${item}")
}

MethodReplaceItem中封装了收集到的字节码属性,同时会根据注解关联方法的字节码指令推断出想要处理的隐私合规 api 的字节码指令。

MethodReplaceItem(List<Object> annotationPair, MethodNode methodNode, String owner) {replaceOpcode = Opcodes.INVOKESTATICreplaceClass = ownerreplaceMethod = methodNode.namereplaceDesc = methodNode.descfor (int i = 0; i < annotationPair.size(); i = i + 2) {def key = annotationPair[i]def value = annotationPair[i + 1]if (key == "targetMethodOpcode") {targetOpcode = value} else if (key == "targetClass") {targetOwner = value} else if (key == "targetName") {targetMethod = value} else if (key == "targetDesc") {targetDesc = value}else if(key == "hook"){willHook = value}}Logger.info("=====targetOpcode:${targetOpcode},targetOwner:${targetOwner} , replaceDesc${replaceDesc}")if (isEmpty(targetMethod)) {targetMethod = replaceMethod}if (isEmpty(targetDesc)) {//静态方法,oriDesc 跟 targetDesc 一样if (targetOpcode == Opcodes.INVOKESTATIC) {targetDesc = replaceDesc} else {//非静态方法,约定第一个参数是实例类名,oriDesc 比 targetDesc 少一个参数,处理一下// (Landroid/telephony/TelephonyManager;)Ljava/lang/String ->  ()Ljava/lang/Stringdef param = replaceDesc.split('\)')[0] + ")"def result = replaceDesc.split('\)')[1]def index = replaceDesc.indexOf(targetOwner)if (index != -1) {param = "(" + param.substring(index + targetOwner.length() + 1)}Logger.info("index::: ${index}")targetDesc = param + result}}
}
2.2.3 合规方法处理 transform

再次扫描整个项目,根据在上一个 transform 中收集到的要处理的隐私合规的 api,遍历字节码指令,当匹配上时,在当前的类中注入写文件的方法,同时在调用隐私合规的字节码指令前插入写文件的字节码指令,用来记录。

@Override
byte[] hookClassInner(String className, byte[] bytes) {Logger.info("${getName()} modifyClassInner--------------->")def findHookPoint = falseMap<MethodNode, InsertInsnPoint> collectMap = new HashMap<>()ClassReader cr = new ClassReader(bytes)ClassNode classNode = new ClassNode()cr.accept(classNode, ClassReader.EXPAND_FRAMES)classNode.methods.each { methodNode ->//过滤掉含有特定注解的方法if (isNotHookMethod(cr.className, methodNode)) {methodNode.instructions.each { insnNode ->//判断字节码能否匹配def methodReplaceItem = searchHookPoint(insnNode)if (methodReplaceItem != null) {//判断是否需要 hook 掉当前指令def inject = methodReplaceItem.willHook//记录隐私合规 api 所在的类及方法logHookPoint(classNode.name, methodReplaceItem, methodNode, insnNode.opcode, insnNode.owner, insnNode.name, insnNode.desc, inject)if (inject) {//hookinjectInsn(insnNode, methodReplaceItem)}//插入写文件方法指令,收集调用隐私方法的堆栈collectInsertInsn(insnNode, methodNode, classNode, collectMap, inject)findHookPoint = true}}}}if (!collectMap.isEmpty() && findHookPoint) {//插入写文件指令,用来展示堆栈信息collectMap.each { key, value ->key.instructions.insert(value.hookInsnNode, value.instList)}//插入 writeToFile 方法ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)classNode.accept(cw)insertWriteToFileMethod(cw)return cw.toByteArray()}return bytes
}

在 collectInsertInsn 方法中,通过 throwable 来收集当前的堆栈

/*** 收集 在调用特定的方法前插入调用写入文件的方法的指令* @param insnNode* @param methodNode* @param classNode* @param collectMap*/
static void collectInsertInsn(insnNode, methodNode, classNode, collectMap, Inject) {def className = classNode.namedef methodName = methodNode.namedef methodDesc = methodNode.descdef owner = nulldef name = nulldef desc = nullif (insnNode instanceof MethodInsnNode) {owner = insnNode.ownername = insnNode.namedesc = insnNode.desc}//------logStringBuilder lintLog = new StringBuilder()lintLog.append(className)lintLog.append("  ->  ")lintLog.append(methodName)lintLog.append("  ->  ")lintLog.append(methodDesc)lintLog.append("\r\n")lintLog.append(owner)lintLog.append("  ->  ")lintLog.append(name)lintLog.append("  ->  ")lintLog.append(desc)//------要插入字节码指令lintLog.append("\r\n")InsnList insnList = new InsnList()insnList.add(new LdcInsnNode(lintLog.toString()))insnList.add(new TypeInsnNode(Opcodes.NEW, "java/lang/Throwable"))insnList.add(new InsnNode(Opcodes.DUP))insnList.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/Throwable", "<init>", "()V", false))insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, className, writeToFileMethodName, writeToFileMethodDesc))println "插入指令完成 =---------->"collectMap.put(methodNode, new InsertInsnPoint(insnList, insnNode))
}

最终,在项目编译完成之后,会在项目的根目录下生成 replaceInsn.txt 文件,记录包含隐私合规 api 的类和相关方法。

当项目运行起来之后,会在设置的路径中(笔者设置在 getExternalCacheDir 中)生成 privacy_log.txt 文件,里面会记录隐私合规 api 的调用堆栈和时间,根据该调用链,我们就可以快速定位是哪一块业务执行了敏感操作。

3.总结

通过 ASM + gradle plugin ,能够排查出大部分的隐私合规问题。有什么不足之处,也请读者多多提意见和建议。

源码地址

https://github.com/season-max/asm_hook.git

相关文章:

ASM 字节码插桩:隐私合规方法检测!

1.前言近两年来工信部对于应用的隐私合规安全问题愈加重视&#xff0c;对 Android 平台的管控程度也要比 IOS 平台严格很多&#xff0c;很多不合规的应用也先后被下架要求整改。笔者就曾遇到过加班整改隐私合规的问题&#xff0c;隐私合规问题主要针对两个方面。在用户同意隐私…...

spring data jpa使用流式查询

思路 调用org.hibernate.query.Query.stream方法查询数据 代码样例 import static org.hibernate.annotations.QueryHints.READ_ONLY; import static org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE; import org.hibernate.query.Query;使用HQL查询 Query<MyEntity> …...

Golang实现RabbitMQ中死信队列各个情况

下面这段教程针对是你已经有一些基本的MQ的知识&#xff0c;比如说能够很清楚的理解queue、exchange等概念&#xff0c;如果你还不是很理解&#xff0c;我建议你先访问官网查看基本的教程。 文章目录1、造成死信队列的主要原因2、操作逻辑图3、代码实战3.1 针对原因1&#xff1…...

react源码分析:组件的创建和更新

这一章节就来讲讲ReactDOM.render()方法的内部实现与流程吧。 因为初始化的源码文件部分所涵盖的内容很多&#xff0c;包括创建渲染、更新渲染、Fiber树的创建与diff&#xff0c;element的创建与插入&#xff0c;还包括一些优化算法&#xff0c;所以我就整个的React执行流程画了…...

Android Lmkd 低内存终止守护程序

一、低内存终止守护程序 Android 低内存终止守护程序 (lmkd) 进程可监控运行中的 Android 系统的内存状态&#xff0c;并通过终止最不必要的进程来应对内存压力大的问题&#xff0c;使系统以可接受的性能水平运行。 所有应用进程都是从zygote孵化出来的&#xff0c;记录在AMS…...

快速掌握 Flutter 图片开发核心技能

大家好&#xff0c;我是 17。 在 Flutter 中使用图片是最基础能力之一。17 做了精心准备&#xff0c;满满的都是干货&#xff01;本文介绍如何在 Flutter 中使用图片&#xff0c;尽量详细&#xff0c;示例完整&#xff0c;包会&#xff01; 使用网络图片 使用网络图片超级简…...

复习使用git(二)

删除远程分支 git push origin --delete 分支名 撤销修改 撤销工作区的修改 已修改&#xff0c;但尚未添加&#xff08;add&#xff09;&#xff0c;使用 git restore 文件名 撤销工作区的修改。 Note: “git checkout – 文件名”&#xff0c;checkout 检出的意思&#x…...

魔兽世界335服务端架设对外网开放的步骤

警告&#xff1a;在没有网络安全防护措施或基础知识的情况下&#xff0c;开放端口可能造成被黑客入侵、流量攻击、破坏数据、资料泄露等情况的发生。在你选择开放端口时&#xff0c;视为已经充分了解可能发生的后果、危害&#xff0c;清楚自己在做什么&#xff0c;并且自己将对…...

华为OD机试模拟题 用 C++ 实现 - 通信误码(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 最多获得的短信条数(2023.Q1)) 文章目录 最近更新的博客使用说明通信误码题目输入输出示例一输入输出说明示例二输入输出说明Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,...

Vue 核心

文章目录Vue 核心一&#xff0c;Vue 简介&#xff08;一&#xff09;官网&#xff08;二&#xff09;介绍与描述&#xff08;三&#xff09;Vue 的特点&#xff08;四&#xff09;与其它 JS 框架的关联&#xff08;五&#xff09;Vue 周边库二&#xff0c;初识 Vue三&#xff0…...

Kylin V10桌面版arm3568 源码安装redis

上传redis-5.0.14.tar.gz到/home/kylin/下载&#xff1b;解压kylinkylin:~/下载$ tar -zxvf redis-5.0.14.tar.gz/opt下新建redis目录&#xff0c;并将上面解压的文件夹移到此处kylinkylin:~/下载$ sudo mv redis-5.0.14 /opt/redis/编译&#xff1a;kylinkylin:/opt/redis/red…...

【ICCV2022】 CAPAO:一种高效的单阶段人体姿态估计模型

CAPAO&#xff1a;一种高效的单阶段人体姿态估计模型 重新思考关键点表示&#xff1a;将关键点和姿态建模作为多人姿态估计的对象&#xff08;Rethinking Keypoint Representations: Modeling Keypoints and Poses as Objects for Multi-Person Human Pose Estimation&#xf…...

ROS1学习笔记:ROS中的坐标管理系统(ubuntu20.04)

参考B站古月居ROS入门21讲&#xff1a;ROS中的坐标系管理系统 基于VMware Ubuntu 20.04 Noetic版本的环境 文章目录一、机器人中的坐标变换二、TF功能包三、小海龟跟随实验3.1 启动实验3.2 查看当前的TF树3.3 坐标相对位置可视化3.3.1 tf_echo3.3.2 rviz一、机器人中的坐标变换…...

requests---(2)session简介与自动写博客

目录&#xff1a;导读 session简介 session登录 自动写博客 获取登录cookies 抓取写博客接口 requests自动写博客 写在最后 http协议是无状态的&#xff0c;也就是每个请求都是独立的。那么登录后的一系列动作&#xff0c;都需要用cookie来验证身份是否是登录状态&#…...

基于 HAProxy + Keepalived 搭建 RabbitMQ 高可用集群

RabbitMQ 集群 通常情况下&#xff0c;在集群中我们把每一个服务称之为一个节点&#xff0c;在 RabbitMQ 集群中&#xff0c;节点类型可以分为两种&#xff1a; 内存节点&#xff1a;元数据存放于内存中。为了重启后能同步数据&#xff0c;内存节点会将磁盘节点的地址存放于磁…...

基于51单片机和proteus的智能调速风扇设计

此智能风扇是基于51单片机和proteus的仿真设计&#xff0c;功能如下&#xff1a; 1. Timer0 PWM控制电机转速 2. DHT11采集温湿度 3. LCD1602显示温湿度及电机状态 4. 按键控制电机加减速启停等 5. 串口控制电机加减速启停等 功能框图如下&#xff1a; Proteus仿真界面如下…...

SQL Server开启CDC的完整操作过程

这里写自定义目录标题写在前面SQL Server开启CDC1. 将指定库的实例先开启CDC2. 开启需要开启CDC的表3. 关闭CDC功能更详细信息参照官网写在前面 鉴于老旧数据的结构和项目都在sqlserver上存储&#xff0c;且迁移成本巨大&#xff0c;当下要为sqlserver的存储过程减负。要将一部…...

【Spring Cloud Alibaba】008-Sentinel

【Spring Cloud Alibaba】008-Sentinel 文章目录【Spring Cloud Alibaba】008-Sentinel一、服务雪崩1、概述2、解决方案常见的容错机制二、Sentinel&#xff1a;分布式系统的流量防卫兵1、**Sentinel** 概述简介特性Sentinel 的开源生态Sentinel 的历史2、Sentinel 基本概念资源…...

解读CRC校验计算

个人随笔 (Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 参考&#xff1a;http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html 参考&#xff1a;https://en.wikipedia.org/wiki/Cyclic_redundancy_check 参考&#xff1a;https://www.cnblogs.com/…...

深入理解Spring MVC下

上一篇博客从理论概念上来梳理Spring MVC相关知识&#xff0c;此篇博客将通过spring官网提供showcase代码为例子&#xff0c;详细介绍showcase代码中包含的各个例子是如何实现的。官网的showcase代码包含的主要例子包括&#xff0c;Demo地址&#xff1a;Mapping Requests&#…...

【Linux】ssh-keygen不需要回车,自动生成密钥,批量免密操作!

使用命令ssh-keygen 需要手动敲击回车&#xff0c;才会生成密钥&#xff0c;如下代码所示 [rootlocalhost ~]# ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/root/.ssh/id_rsa): Enter passphrase (empty for no passphrase):…...

C/C++开发,无可避免的内存管理(篇四)-智能指针备选

一、智能指针 采用C/C开发堆内存管理无论是底层开发还是上层应用&#xff0c;无论是开发新手&#xff0c;还是多年的老手&#xff0c;都会不自觉中招&#xff0c;尤其是那些不是自己一手经历的代码&#xff0c;要追溯问题出在哪里更是个麻烦事。C/C程序常常会遇到程序突然退出&…...

VMware ESXi给虚拟机扩容

用ESXi管理的虚拟机硬盘空间不够了&#xff0c;讲一下如何进行扩容。 一、查看现状 通过如下三个命令&#xff0c;可以查看硬盘情况&#xff0c;可以看到只有500G&#xff0c;已经用了45%。这次我们再扩容500G。 df -Th lsblk fdisk -lIDE磁盘的文件名为        /de…...

认识STM32和如何构建STM32工程

STM32介绍什么是单片机单片机(Single-Chip Microcomputer)是一种集成电路芯片&#xff0c;把具有数据处理能力的中央处理器CPU、随机存储器RAM、只读存储器ROM、多种/0口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电…...

RabbitMQ延迟队列

目录 一、概念 二、使用场景 三、RabbitMQ 中的 TTL &#xff08;一&#xff09;队列设置 TTL &#xff08;二&#xff09;消息设置 TTL &#xff08;三&#xff09;两者的区别 四、整合SpringBoot实现延迟队列 &#xff08;一&#xff09;创建项目 &#xff08;二&am…...

Java中常用的七种队列你了解多少?

文章目录Java中常用的七种队列你了解多少?ArrayBlockingQueue队列如何使用&#xff1f;添加元素到队列获取队列中的元素遍历队列LinkedBlockingQueue队列如何使用&#xff1f;1. 创建SynchronousQueue对象2. 添加元素到队列3. 获取队列中的元素4. 遍历队列SynchronousQueue队列…...

<Java获取时间日期工具类>常见八种场景(一)

一:自定义时间日期工具类常用的八种方式&#xff08;整理&#xff09;: 0&#xff0c;getTimeSecondNum&#xff1a;时间日期转成秒数&#xff0c;常用于大小比较 1&#xff0c;getLastYearMonthLastDay&#xff1a;获取去年当月最后一天的时间日期 2&#xff0c;getLastYearM…...

接上一篇 对多个模型环形旋转进行优化 指定旋转位置

using System.Collections; using System.Collections.Generic; using UnityEngine; using DG.Tweening; public class ModelAnimal : MonoBehaviour { //记录鼠标滑动 public Vector2 lastPos;//鼠标上次位置 Vector2 currPos;//鼠标当前位置 Vector2 offset;//两次位置的偏移…...

Unity中获取地形的法线

序之前&#xff0c;生成了地形图&#xff1a;(42条消息) 从灰度图到地形图_averagePerson的博客-CSDN博客那末&#xff0c;地形的法线贴图怎么获取&#xff1f;大概分为两个部分吧&#xff0c;先拿到法线数据&#xff0c;再画到纹理中去。关于法线计算Unity - Scripting API: M…...

模型解释性:PFI、PDP、ICE等包的用法

本篇主要介绍几种其他较常用的模型解释性方法。 1. Permutation Feature Importance(PFI) 1.1 算法原理 置换特征重要性(Permutation Feature Importance)的概念很简单&#xff0c;其衡量特征重要性的方法如下&#xff1a;计算特征改变后模型预测误差的增加。如果打乱该特征的…...

做门头上那个网站申报/广州最新发布最新

分析redis key的大小 bigKeys 这是redis-cli自带的一个命令。对整个redis进行扫描&#xff0c;寻找较大的key。例&#xff1a; 格式&#xff1a;redis-cli -h 服务端主机名或者IP地址 -p 端口 [-a password] --bigkeys $ redis-cli -h 192.168.0.112 -p 6379 --bigkeys …...

怎么建com的网站/广州网站建设推广专家

我想得到按流量来排序&#xff0c;而且还是倒序&#xff0c;怎么达到实现呢&#xff1f; 达到下面这种效果&#xff0c; 默认是根据key来排&#xff0c; 我想根据value里的某个排&#xff0c; 解决思路:将value里的某个&#xff0c;放到key里去&#xff0c;然后来排 下面&#…...

中学生制作网站/北京seo经理

模块化程序设计之函数的递归调用 前言 C语言是结构化、模块化的编程语言&#xff0c;可以蒋一个大程序分割成若干相对独立的功能模块&#xff0c;每个模块使用函数来实现&#xff0c;并通过函数调用以及数据传递来实现整体大程序的功能。 1、函数的递归调用 函数的递归调用&am…...

11网拍推广平台/seo诊断站长

2019独角兽企业重金招聘Python工程师标准>>> 1、firewalld的基本使用 启动&#xff1a; systemctl start firewalld 关闭&#xff1a; systemctl stop firewalld 查看状态&#xff1a; systemctl status firewalld 开机禁用 &#xff1a; systemctl disable firew…...

ppt的网站导航栏怎么做的/快速的网站设计制作

1.在Mathpix官网下载安装Mathpix软件。 2.下载安装MathType&#xff0c;因为Mathpix只能把公式转换为LaTeX格式&#xff0c;这个时候需要MathType将这种特殊格式再转换回我们直观的公式形式。 3.打开WPS—>插入—>对象—>MathType 4.运行Mathpix软件&#xff0c;使用…...

临淄网站建设/小程序怎么开发自己的小程序

格式化是很常见的数据恢复案例故障&#xff0c;如果被格式化的盘是有重要的文件&#xff0c;那么一定要注意马上停止往这个盘写入文件。然后用数据恢复软件扫描恢复里面的数据。具体的恢复方法可以看下文了解。 工具/软件&#xff1a;AuroraDataRecovery 步骤1&#xff1a;先百…...