41.安卓逆向2-frida hook技术-过firda检测(五)-利用ida分析app的so文件中frida检测函数过检测

免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!

内容参考于:图灵Python学院

工具下载:

链接:https://pan.baidu.com/s/1bb8NhJc9eTuLzQr39lF55Q?pwd=zy89

提取码:zy89

复制这段内容后打开百度网盘手机App,操作更方便哦

上一个内容:40.安卓逆向2-frida hook技术-过firda检测(四)(通过拦截so文件的创建和拦截检测frida函数过检测)

本次通过查找app中检测frida的函数,然后使用frida对检测frida的函数进行hook,通过hook让它失效来绕过检测

首先还是通过hook加载so文件的函数,看看它是加载到什么文件时进行的退出,然后使用ida反编译so文件,下方是检测app加载so文件的frida代码

function main(){// 这段代码是给一个叫"Frida"的工具用的脚本
// Frida的作用是:可以钻进手机里的APP内部,看看这个APP在偷偷做什么
// 我们这段脚本的具体任务是:盯着APP加载"特殊文件"的行为// 首先,我们要找到APP加载文件时会用到的两个"工具函数"// 第一个工具函数叫"dlopen"
// 所有运行在Linux或安卓系统上的程序,要加载"动态链接库"(一种特殊文件,后缀通常是.so)时,经常会用到它
// "Module.findExportByName(null, "dlopen")"的作用:
// 1. 在系统的所有功能里找(null表示不限制范围)
// 2. 找到名字叫"dlopen"的那个功能,记录下它在内存中的位置
// 3. 把找到的结果存在变量"dlopen"里,方便后面使用
var dlopen = Module.findExportByName(null, "dlopen");// 第二个工具函数叫"android_dlopen_ext"
// 这是安卓系统专门设计的加强版加载工具,功能比dlopen更多一点
// 有些安卓APP会用这个函数来加载特殊的.so文件
// 下面这行代码的作用和上面类似:找到这个函数的位置,存在变量里
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");// 接下来,我们要给第一个工具函数"dlopen"装个"监控器"
// 当APP调用dlopen加载文件时,我们就能立刻知道
// "Interceptor.attach"就是Frida提供的"装监控器"的功能
Interceptor.attach(dlopen, {// 当APP刚开始调用dlopen函数时,会自动执行下面的代码// 可以理解为:监控器发现"有人要开始用这个工具了"onEnter: function (args) {// "args"是APP传给dlopen的参数(就像我们给工具传递的"指令")// dlopen的第一个参数很重要:它告诉工具"要加载的文件在哪里"// 这里的"args[0]"就是取第一个参数(计算机里计数从0开始)var path_ptr = args[0];// 刚才拿到的"path_ptr"其实是个"内存地址"(类似文件在仓库里的货架编号)// 我们需要根据这个编号,找到实际的文件路径(比如"/data/lib/test.so")// "ptr(path_ptr)"是把编号转成Frida能识别的格式// ".readCString()"是按照计算机存储文字的规则,把地址对应的内容读出来var path = ptr(path_ptr).readCString();// 最后,把我们发现的信息打印到屏幕上// 这样我们就能清楚地看到:这个APP用dlopen加载了哪个文件console.log("[发现使用dlopen加载文件:] ", path);},// 当APP用完dlopen函数(加载文件完成后),会执行下面的代码// 可以理解为:监控器发现"这个人用完工具了"onLeave: function (retval) {// 目前这里什么都没做,留空是因为我们暂时只关心"开始加载"这个动作// 如果以后想知道"加载成功了吗",可以在这里处理返回值retval}
});// 下面是给第二个工具函数"android_dlopen_ext"装监控器,原理和上面完全一样
Interceptor.attach(android_dlopen_ext, {// 当APP刚开始调用这个安卓特有的加载函数时onEnter: function (args) {// 同样取第一个参数:要加载的文件地址var path_ptr = args[0];// 把地址转成我们能看懂的文件路径var path = ptr(path_ptr).readCString();// 打印信息:APP用安卓特有的工具加载了哪个文件console.log("[发现使用安卓专用dlopen_ext加载文件:] ", path);},// 当APP用完这个函数后onLeave: function (retval) {// 这里也暂时什么都不做}
});
/**
总结代码的效果:就像我们在 APP 的 "文件加载通道" 上装了两个摄像头,一个盯着普通加载通道,一个盯着安卓专用通道。
只要 APP 从这些通道加载文件(特别是.so 格式的文件),摄像头就会立刻拍下 "文件地址" 并显示出来,让
我们清楚知道这个 APP 在运行时偷偷加载了哪些底层文件。
这种监控在分析 APP 的工作原理、查找恶意软件行为时非常有用。
*/
}
main()

如下图注入上方的代码后,在加载了下图红框的so文件后,frida退出了,这说明 libmsaoaidsec.so 里面有frida检测

查看线程,从下图红框可以看出,libmsaoaidsec.so创建了三个线程,然后退出了,这说明检测frida的代码在这三个线程中,记住这三个值一会要用, 1c544、1b8d4、 26e5c

// 定义一个函数,名字叫 hook_patch,作用是"钩住"线程创建的行为
function hook_patch() {
// 1. 找到系统里负责创建线程的函数(pthread_create)的地址
// 解释:
// - pthread_create 是 Linux/Android 系统中创建线程的核心函数,所有程序创建线程都要调用它
// - Module.findExportByName("libc.so", "pthread_create") 意思是:从 libc.so 这个系统库中,查找导出的 pthread_create 函数的地址
// - libc.so 是系统基础库,包含了很多常用的系统函数(比如创建线程、文件操作等)
var patch = Module.findExportByName("libc.so", "pthread_create");// 2. 打印找到的 pthread_create 函数的地址(调试用,确认是否找到了目标函数)
// 比如可能会输出:[pth_create] 0x7f8a8b2c3d40(这是一个内存地址)
console.log("[pth_create]", patch);// 3. 拦截(Hook)这个 pthread_create 函数,监控它的调用
// Interceptor.attach 是 Frida 的拦截函数,第一个参数是要拦截的函数地址(这里就是上面找到的 patch)
// 第二个参数是一个对象,里面定义了拦截后的行为(进入函数时做什么,离开函数时做什么)
Interceptor.attach(patch, {// onEnter:当被拦截的函数(pthread_create)被调用时,会执行这里的代码onEnter: function (args) {// args 是一个数组,存放了调用 pthread_create 时传入的参数// pthread_create 的函数原型是:// int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg)// 所以 args[0] = 线程ID指针,args[1] = 线程属性,args[2] = 线程要执行的函数(核心!线程启动后会跑这个函数),args[3] = 传给线程函数的参数// 4. 通过线程要执行的函数地址(args[2]),找到它属于哪个模块(.so 文件)// Process.findModuleByAddress(地址) 会返回这个地址所在的模块信息(比如模块名、路径等)var module = Process.findModuleByAddress(args[2]);// 5. 检查是否成功找到模块(避免空值报错)if (module != null) {// 打印线程相关信息:// - module.name:模块的名字(比如 libnative.so,就是这个模块创建了线程)// - args[2].sub(module.base):计算线程函数在模块中的偏移量(相对位置)//   偏移量 = 函数的实际地址 - 模块的基地址(模块加载到内存的起始地址)//   比如模块基地址是 0x1000,函数地址是 0x1200,偏移量就是 0x200console.log("开启线程-->", module.name, args[2].sub(module.base));}},// onLeave:当被拦截的函数(pthread_create)执行完毕,准备返回时,会执行这里的代码// 这里暂时为空,说明不需要处理函数返回后的逻辑onLeave: function (retval) {}
});
}
function main(){
// 这段代码是给一个叫"Frida"的工具用的脚本
// Frida的作用是:可以钻进手机里的APP内部,看看这个APP在偷偷做什么
// 我们这段脚本的具体任务是:盯着APP加载"特殊文件"的行为// 首先,我们要找到APP加载文件时会用到的两个"工具函数"// 第一个工具函数叫"dlopen"
// 所有运行在Linux或安卓系统上的程序,要加载"动态链接库"(一种特殊文件,后缀通常是.so)时,经常会用到它
// "Module.findExportByName(null, "dlopen")"的作用:
// 1. 在系统的所有功能里找(null表示不限制范围)
// 2. 找到名字叫"dlopen"的那个功能,记录下它在内存中的位置
// 3. 把找到的结果存在变量"dlopen"里,方便后面使用
var dlopen = Module.findExportByName(null, "dlopen");// 第二个工具函数叫"android_dlopen_ext"
// 这是安卓系统专门设计的加强版加载工具,功能比dlopen更多一点
// 有些安卓APP会用这个函数来加载特殊的.so文件
// 下面这行代码的作用和上面类似:找到这个函数的位置,存在变量里
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");// 接下来,我们要给第一个工具函数"dlopen"装个"监控器"
// 当APP调用dlopen加载文件时,我们就能立刻知道
// "Interceptor.attach"就是Frida提供的"装监控器"的功能
Interceptor.attach(dlopen, {
// 当APP刚开始调用dlopen函数时,会自动执行下面的代码
// 可以理解为:监控器发现"有人要开始用这个工具了"
onEnter: function (args) {// "args"是APP传给dlopen的参数(就像我们给工具传递的"指令")// dlopen的第一个参数很重要:它告诉工具"要加载的文件在哪里"// 这里的"args[0]"就是取第一个参数(计算机里计数从0开始)var path_ptr = args[0];// 刚才拿到的"path_ptr"其实是个"内存地址"(类似文件在仓库里的货架编号)// 我们需要根据这个编号,找到实际的文件路径(比如"/data/lib/test.so")// "ptr(path_ptr)"是把编号转成Frida能识别的格式// ".readCString()"是按照计算机存储文字的规则,把地址对应的内容读出来var path = ptr(path_ptr).readCString();// 最后,把我们发现的信息打印到屏幕上// 这样我们就能清楚地看到:这个APP用dlopen加载了哪个文件console.log("[发现使用dlopen加载文件:] ", path);},
// 当APP用完dlopen函数(加载文件完成后),会执行下面的代码
// 可以理解为:监控器发现"这个人用完工具了"
onLeave: function (retval) {// 目前这里什么都没做,留空是因为我们暂时只关心"开始加载"这个动作// 如果以后想知道"加载成功了吗",可以在这里处理返回值retval
}
});// 下面是给第二个工具函数"android_dlopen_ext"装监控器,原理和上面完全一样
Interceptor.attach(android_dlopen_ext, {
// 当APP刚开始调用这个安卓特有的加载函数时
onEnter: function (args) {// 同样取第一个参数:要加载的文件地址var path_ptr = args[0];// 把地址转成我们能看懂的文件路径var path = ptr(path_ptr).readCString();// 打印信息:APP用安卓特有的工具加载了哪个文件console.log("[发现使用安卓专用dlopen_ext加载文件:] ", path);if(path.indexOf("libmsaoaidsec.so")!=-1){// 调用查看线程的函数hook_patch()}
},
// 当APP用完这个函数后
onLeave: function (retval) {// 这里也暂时什么都不做
}
});
/**
总结代码的效果:就像我们在 APP 的 "文件加载通道" 上装了两个摄像头,一个盯着普通加载通道,一个盯着安卓专用通道。
只要 APP 从这些通道加载文件(特别是.so 格式的文件),摄像头就会立刻拍下 "文件地址" 并显示出来,让
我们清楚知道这个 APP 在运行时偷偷加载了哪些底层文件。
这种监控在分析 APP 的工作原理、查找恶意软件行为时非常有用。
*/
}
main()

然后把apk进行解压,找到它的 libmsaoaidsec.so 文件拖到ida中进行反编译

然后ida加载完后,点击下图红框,然后按CTRL+F

然后搜索1c544、1b8d4、 26e5c这三个,如下图首先是搜索1c544,双击下图红框位置就可以跳转到1c544了,跳转之后按f5转伪c代码

直接把下图红框的代码全选,然后复制给ai大模型,让它解释

如下图ai的解释,这个 1c544 做的是字符串相关和定时任务(无限循环执行周期性任务),跟检测frida无关,所以下一个

然后搜索1b8d4,老样子全选复制给ai大模型

如下图大模型的解释,它有点可疑,根据大模型的解释,1b8d4会检测,还会暂停,它可能是检测firda然后暂停frida

然后接着看最后一个 26e5c

大模型的解释,三个线程都分析完了,现在只有 1b8d4 是最可疑的,接下来通过调用进一步分析(它有一个特征)

再次回到1b8d4 中,鼠标点击下图红框位置,然后按x

可以看谁调用了1b8d4,如下图有两个位置调用了它,但都是在1B924中调用的

1B924 调用 1b8d4,然后双击上图任意一个,然后点击下图红框位置,再次按x,查看 1B924 是谁调用的

如下图只有一个地方调用了 1B924

然后现在的调用栈是 1BEC4 调用 1B924 调用 1b8d4

继续重复上方的步骤,按 x 查看 1BEC4 谁调用的,如下图只有一个位置对1BEC4进行了调用,然后双击它进入函数

然后特征就来了,如下调用1BEC4的函数叫做init_proc,现在的调用栈 init_proc 调用 1BEC4 调用 1B924 调用 1b8d4

然后 init_proc 不懂没关系,给ai大模型,让它解释,如下图ai大模型的结束,很详细,所以 1b8d4 它必然是检测frida的相关函数,然后 1b8d4 里面只做了暂停操作,没有关闭frida的操作,所以调用 1b8d4 函数的1B924 函数才是检测frida的函数,接下来只需要把 1B924 函数进行hook就可以过检测了

hook代码,注意 var 构造函数调用偏移 这个值,可能每个手机不一样,它的找法继续往下看,写后面了

// 主函数:根据IDA中获取的地址信息,替换目标SO中的指定函数
function 根据IDA地址替换函数() {// 1. 获取linker64模块的基地址// 来源:linker64是安卓系统自带的64位动态链接器(负责加载所有SO文件),固定名称为"linker64"// 作用:基地址是模块加载到内存的起始位置,类似"小区大门的地址"var 链接器64基地址 = Module.getBaseAddress("linker64")// 打印基地址用于调试(实际值会随系统/进程变化,例如0x7f8a0000)console.log("linker64模块基地址(内存起始位置):", 链接器64基地址)// 2. 定义call_constructors函数的偏移量// 来源:这个值必须从IDA中获取!步骤是://   a. 用IDA打开目标设备的linker64文件(通常在/system/bin/linker64)//   b. 搜索函数名"call_constructors"//   c. 记录该函数相对于linker64基地址的偏移(例如0x50C00)// 作用:偏移量是函数在模块内部的位置,类似"小区内某栋楼的门牌号"var 构造函数调用偏移 = 0x50C00  // 这里必须替换为你从IDA中看到的实际值!// 3. 计算call_constructors函数的实际内存地址// 公式:实际地址 = 模块基地址 + 偏移量(类似"小区大门地址 + 门牌号 = 具体住户地址")// 例如:基地址0x7f8a0000 + 偏移0x50C00 = 实际地址0x7f8f0C00var 构造函数调用地址 = 链接器64基地址.add(构造函数调用偏移)console.log("call_constructors函数实际地址:", 构造函数调用地址)// 4. 拦截call_constructors函数// 原因:这个函数是linker64加载SO文件时,用于初始化SO中构造函数的关键函数// 目的:在目标SO(libmsaoaidsec.so)加载完成并初始化时,及时执行替换操作var 拦截器 = Interceptor.attach(构造函数调用地址, {// 当call_constructors函数被调用时(即有SO正在初始化),执行以下代码onEnter: function (参数列表) {console.log("检测到SO文件正在初始化(进入call_constructors函数)")// 5. 检查目标SO是否已加载// 来源:"libmsaoaidsec.so"是你要操作的目标SO文件名(需替换为你的实际SO名)// 作用:确认我们要修改的SO已经被系统加载到内存中var 目标模块 = Process.findModuleByName("libmsaoaidsec.so")// 6. 如果目标SO已加载,则执行替换if (目标模块 != null) {console.log("目标SO已加载:" + 目标模块.name + ",基地址:" + 目标模块.base)// 7. 替换目标SO中的指定函数// ① 目标函数地址计算://    来源:0x1B924是从IDA中获取的目标函数偏移量,步骤://      a. 用IDA打开libmsaoaidsec.so//      b. 找到你要替换的函数(例如sub_1B924)//      c. 记录该函数相对于SO基地址的偏移//    公式:目标函数实际地址 = 目标SO基地址 + 偏移量// ② 新函数定义://    返回值类型"void"和参数列表[]必须与原函数一致(从IDA中查看函数原型获取)Interceptor.replace(目标模块.base.add(0x1B924),  // 目标函数的实际内存地址new NativeCallback(function () {  // 替换后的新函数console.log("目标函数(偏移0x1B924)已被成功替换!")// 这里可以添加自定义逻辑,例如:// - 返回固定值(如return 0;)// - 修改原函数参数(需在参数列表中定义)// - 调用原函数后修改返回值}, "void",  // 新函数返回值类型(必须与原函数一致,从IDA中查)[]       // 新函数参数列表(必须与原函数一致,从IDA中查)))// 8. 替换完成后解除拦截// 原因:避免后续加载其他SO时重复执行替换操作拦截器.detach()console.log("已完成替换,解除对call_constructors的拦截")}},// 函数执行结束时的操作(这里不需要,留空)onLeave: function (返回值) {}})
}// 执行主函数,启动整个替换流程
根据IDA地址替换函数()

如下图成功绕过检测,app正常启动

然后 构造函数调用偏移 值的找法,把下图红框的文件,使用 adb pull 下载到电脑上

adb pull /system/bin/linker64 xxxxx

上方的指令执行完后,如下图,就把 linker64 文件下载到电脑上了

然后把它拖到ida中,然后搜索 constructor,下图红框的就是我们要找的,

这个函数的地址50C00

不分析的方式过检测,直接把 1c544、1b8d4、 26e5c 这三个线程全返回空

// 定义一个函数,用于将指定地址的代码替换为"直接返回"
// 作用:让目标函数被调用时直接退出,不执行原来的逻辑
function nop_addr(addr) {// 第一步:修改内存权限为"可读可写可执行"(rwx)// 原因:默认情况下代码段可能没有写权限,无法修改指令// 参数说明:// - addr:要修改的内存地址// - 4:修改的内存大小(4字节,足够存放一条返回指令)// - 'rwx':新的权限(read/write/execute)Memory.protect(addr, 4 , 'rwx');// 第二步:创建一个Arm64架构的指令写入器// 注意:这里假设目标设备是64位ARM架构(手机几乎都是ARM)var w = new Arm64Writer(addr);// 第三步:写入"返回指令"(ret)// 效果:当程序执行到这里时,会直接退出当前函数,不执行后续代码w.putRet();// 第四步:刷新写入的指令(确保生效)w.flush();// 第五步:释放写入器资源(避免内存泄漏)w.dispose();
}// 定义主函数:Hook动态链接器的构造函数调用流程,监控目标SO加载
function hook_call_constructors() {// 声明变量:用于存储动态链接器(linker)的模块信息let linker = null;// 判断当前进程是32位还是64位// Process.pointerSize是指针大小:32位系统为4字节,64位为8字节if (Process.pointerSize === 4) {// 32位系统的动态链接器名为"linker"linker = Process.findModuleByName("linker");} else {// 64位系统的动态链接器名为"linker64"(大部分现代手机是64位)linker = Process.findModuleByName("linker64");}// 声明变量:存储找到的关键函数地址// call_constructors_addr:SO初始化函数的地址// get_soname:获取SO文件名的函数(这里未实际使用)let call_constructors_addr, get_soname;// 枚举动态链接器模块中的所有符号(符号=函数名/变量名+地址)// 作用:从链接器中找到我们需要监控的函数let symbols = linker.enumerateSymbols();// 遍历所有符号,筛选出需要的函数for (let index = 0; index < symbols.length; index++) {let symbol = symbols[index];// 匹配"__dl__ZN6soinfo17call_constructorsEv"符号// 这个符号对应的函数是:动态链接器加载SO时,执行SO内部构造函数的入口// 来源:安卓系统动态链接器的标准符号,可通过符号表查询到if (symbol.name === "__dl__ZN6soinfo17call_constructorsEv") {call_constructors_addr = symbol.address; // 记录这个函数的内存地址} // 匹配"__dl__ZNK6soinfo10get_sonameEv"符号(可选,备用)// 作用:通过这个函数可以获取当前正在加载的SO的文件名else if (symbol.name === "__dl__ZNK6soinfo10get_sonameEv") {// 将函数地址包装为NativeFunction,方便后续调用// 参数说明:// - symbol.address:函数的内存地址// - "pointer":函数返回值类型(返回SO文件名的字符串地址)// - ["pointer"]:函数参数(传入soinfo结构体的指针)get_soname = new NativeFunction(symbol.address, "pointer", ["pointer"]);}}// 打印找到的call_constructors函数地址(调试用,确认是否找到)console.log("call_constructors函数地址:", call_constructors_addr);// 拦截call_constructors函数:监控所有SO的初始化过程var listener = Interceptor.attach(call_constructors_addr, {// 当call_constructors函数被调用时触发(有SO正在加载初始化)onEnter: function (args) {console.log("检测到SO文件正在初始化(进入call_constructors)");// 检查我们关注的目标SO(libmsaoaidsec.so)是否已加载// 注意:这里的SO文件名需要替换为你实际要操作的SO名称var module = Process.findModuleByName("libmsaoaidsec.so");// 如果目标SO已经加载到内存中if (module != null) {console.log("找到目标SO:" + module.name + ",基地址:" + module.base);// 对目标SO中的三个关键函数执行"替换为返回"操作// 0x1c544、0x1b8d4、0x26e5c是函数在SO中的偏移量// 偏移量来源:通过IDA/ Ghidra等反编译工具分析目标SO得到// 计算实际地址公式:实际地址 = SO基地址(module.base) + 偏移量nop_addr(module.base.add(0x1c544)); // 处理第一个函数console.log("0x1c544: 已替换为返回指令(函数被跳过)");nop_addr(module.base.add(0x1b8d4)); // 处理第二个函数console.log("0x1b8d4: 已替换为返回指令(函数被跳过)");nop_addr(module.base.add(0x26e5c)); // 处理第三个函数console.log("0x26e5c: 已替换为返回指令(函数被跳过)");// 完成替换后,解除对call_constructors的拦截// 原因:避免后续加载其他SO时重复执行替换操作listener.detach();console.log("所有目标函数处理完毕,已解除拦截");}},// 函数执行结束时的操作(这里不需要处理,留空)onLeave: function (retval) {}});
}// 执行主函数,启动整个Hook流程
hook_call_constructors();

img

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

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

相关文章

安卓调javaScript Not find method “forceLogout“ implementatidsignature or namesp

核对一下是否实现对应的javaScript或者javaScript的方法参数对不对&#xff0c; 在这里插入图片描述我这里一开始实现了这个方法但是没有给参数&#xff0c;一直报异常&#xff0c;后台说token没给就查token的问题&#xff0c;最后发现是搞偏了&#xff0c;两个原因&#xff0c…

【Linux网络】:UDP(传输层协议)

目录 一、铺垫知识 1、传输层 2、端口号 2.1、五元组表示 一个进程通信 2.2、端口号范围划分 2.3、知名端口 2.4、查看端口号 2.5、问题 3、pidof & netstat 命令 ①netsate 命令 ②pidof命令 二、UDP协议 1、UDP协议格式 2、UDP报文 1.1、UDP数据封装的过…

Effective C++ 条款19: 设计class犹如设计type

Effective C 条款19&#xff1a;设计class犹如设计type核心思想&#xff1a;设计新的class时&#xff0c;应当像语言设计者设计内置类型一样慎重&#xff0c;考虑对象的创建、销毁、初始化、拷贝、类型转换等所有方面。 ⚠️ 1. 类设计的关键问题域 对象生命周期管理&#xff1…

《汇编语言:基于X86处理器》第11章 MS-Windows编程(3)

本章展示的是如何用32 位Microsoft Windows API进行控制台窗口编程。应用编程接口(API:ApplicationProgramming Interface)是类型、常数和函数的集合体&#xff0c;它提供了一种用计算机代码操作对象的方式。本章将讨论文本I/O、颜色选择、时间与日期、数据文件I/O&#xff0c;…

在 macOS 上通过 Docker 部署DM8 (ARM 架构)

概述 达梦数据库 (DM8) 无法直接在 Apple macOS 操作系统上原生安装&#xff0c;通常需要通过虚拟机&#xff08;如 Parallels Desktop、VMware Fusion&#xff09;进行部署。另一种更轻量级且受 macOS 支持的方案是利用 Docker 容器技术来构建开发与测试环境。本文档将详细介…

网络协议之路由是怎么回事?

写在前面 要想去外面的世界看看, 就离不了路由器&#xff0c;而路由器工作的原理就是路由&#xff0c;那么具体是怎么路由的呢&#xff1f;本文就一起来看下这部分内容。 1&#xff1a;路由的配置 配置一条路由无非就是在配置以下三个信息&#xff1a; 1:包要去哪里&#x…

2106. 摘水果,梳理思路

文章目录题目概要java 解法详解题目概要 在一个无限的 x 坐标轴上&#xff0c;有许多水果分布在其中某些位置。给你一个二维整数数组 fruits &#xff0c;其中 fruits[i] [positioni, amounti] 表示共有 amounti 个水果放置在 positioni 上。fruits 已经按 positioni 升序排列…

深入理解消息队列(MQ)核心原理与设计精髓

引言&#xff1a;从一个“不堪重负”的订单系统说起想象一个简化的电商下单流程&#xff1a;用户点击“下单”后&#xff0c;系统需要&#xff1a;在订单数据库中创建一条记录。调用库存服务&#xff0c;扣减商品库存。调用营销服务&#xff0c;给用户发放积分和优惠券。调用通…

前端手撕题总结篇(算法篇——来自Leetcode牛客)

链表指定区域反转 找到区间&#xff08;头和为 for循环当**时&#xff09;->反转链表&#xff08;返回反转过后的头和尾&#xff09;->连接 function reverseBetween( head , m , n ) {//preEnd&cur&nextStart cur.next断开if(mn)return head;const vHeadNode…

从Excel到工时管理系统:企业如何选择更高效的工时记录工具?

还在为手工统计员工工时而头疼吗&#xff1f;月末堆积如山的Excel表格、反复核对的数据、层出不穷的差错&#xff0c;这些问题正在拖慢企业的发展步伐。8Manage工时管理系统发现&#xff0c;传统手工记录不仅耗费大量人力&#xff0c;更让宝贵的工时数据难以转化为有效的管理决…

Java设计模式之《命令模式》

目录 1、介绍 1.1、命令模式定义 1.2、对比 1.3、典型应用场景 2、命令模式的结构 2.1、组成部分&#xff1a; 2.2、整体流程 3、实现 3.1、没有命令模式 3.2、命令模式写法 4、命令模式的优缺点 前言 java设计模式分类&#xff1a; 1、介绍 1.1、命令模式定义 命…

【动态规划算法】路径问题

什么是动态规划算法动态规划&#xff08;Dynamic Programming&#xff0c;简称 DP&#xff09;是一种通过分解复杂问题为重叠子问题&#xff0c;并存储子问题的解以避免重复计算&#xff0c;从而高效求解具有特定性质&#xff08;重叠子问题、最优子结构&#xff09;问题的算法…

Java基本技术讲解

一、基础语法三要素 暂时无法在飞书文档外展示此内容 &#x1f511; 黄金法则​&#xff1a;每个变量都要声明类型&#xff01;二、程序逻辑控制&#xff08;游戏行为核心&#xff09; 条件判断&#xff1a;if-else - “岔路口选择” // 捡到金币逻辑 if (isTouching(Coin.clas…

【网络基础2】路由器的 “两扇门”:二层接口和三层接口到底有啥不一样?

目录 前言:路由器不是只有 “插网线的口” 一、先搞懂一个基础:路由器是 “网络交通枢纽” 二、二层接口:“小区内部的单元门”,只认 “住户身份证” 1. 啥是二层接口? 2. 用 “小区内部串门” 理解二层接口 步骤 1:手机打包数据,写上 “收件人身份证” 步骤 2:二…

MLIR TableGen

简介 TableGen 是一种领域特定语言&#xff08;DSL&#xff09;&#xff0c;TableGen 的设计目标是允许编写灵活的描述&#xff0c;并将记录的通用特性提取出来&#xff0c;从而减少重复代码并提高代码的可维护性。 TableGen的工作流程&#xff1a; 前端解析&#xff1a; Ta…

2、docker容器命令 | 信息查看

1、命令总览命令作用docker ps查看运行中的容器&#xff08;-a查看所有容器&#xff09;docker logs [CONTAINER]查看容器日志&#xff08;-f实时追踪日志&#xff09;docker inspect [CONTAINER]查看容器详细信息&#xff08;JSON格式&#xff09;docker stats [CONTAINER]实时…

【MySQL】MySQL中锁有哪些?

一、按照粒度分类&#xff1a; 粒度越小&#xff0c;并发度越高&#xff0c;锁开销越大。 1.全局锁&#xff1a; 作用&#xff1a; 锁定整个MySQL实例(所有数据库)。适用场景&#xff1a; 全库逻辑部分。(确保备份期间数据的一致性。)实现方式&#xff1a; 通过 FLUSH TABLES W…

语义分割--deeplabV3+

根据论文网络结构图讲一下&#xff1a;网络分为两部分&#xff1a;encoder和decoder部分。 Encoder&#xff1a;DCNN就是主干网络&#xff0c;例如resnet&#xff0c;Xception&#xff0c;MobileNet这些&#xff08;主干网络也要使用空洞卷积&#xff09;&#xff0c;对dcnn的结…

Azure DevOps 中的代理

必知词汇 深入研究 Azure DevOps 中的代理之前需要掌握的基本概念: 代理:Azure DevOps 中的代理是一个软件组件,负责执行流水线中的任务和作业。这可能包括数据中心内的物理服务器、本地或云端托管的虚拟机,甚至是容器化环境。这些代理可以在各种操作系统和环境中运行,例如…

AUTOSAR进阶图解==>AUTOSAR_SRS_ADCDriver

AUTOSAR ADC驱动详解 基于AUTOSAR标准的ADC驱动模块需求规范分析目录 ADC驱动模块概述 关键概念定义 ADC驱动架构 ADC驱动在AUTOSAR分层架构中的位置ADC驱动的主要职责 ADC驱动配置结构 通用配置(AdcGeneral)硬件单元配置(AdcHwUnit)通道配置(AdcChannel)通道组配置(AdcChanne…