个人曾经ARM64_汇编角度_PLTHOOK的研究

server/2025/1/15 17:51:11/

ARM64基础HOOK研究_2024

之前为了实现一个修改器变速器的小功能,结果研究了很多关于ELF的内容,特别是so文件(ARM64的)
还研究了Hook,以及注入进程等操作,以及实现类似IDA那样的断点,汇编转换,以及软硬断点等(实现了CE那种谁写入/访问/读取的检测),这里就不作记录了,只记录一下简单的hook研究经历
我个人是比较喜欢研究底层原理的,所以搞了这些> _ <,之前目标是研究IDA调试App的so的原理,想着实现,边调试边Hook,然后一发不可收拾了…今天整理电脑文件,所以就想发点记录
如今,告别曾经的自己,再也不搞了,太浪费时间了> _ <
在这里插入图片描述

**在这里插入图片描述**

2024年研究的HOOK,现在翻出来,做个记录吧(恭送之前的自己)

inlineHOOK

// 原理
// 替换函数指令实现跳转[小范围跳转]
//	最大跳转范围	前后64MB大小
uint_fast64_t mask = 0x03ffffffu;//跳转指令 B BL BLR 指令
/*
B 指令:跳转到标签处,不保存返回地址,不设置链接寄存器。
BR 指令:跳转到寄存器中的地址,不保存返回地址,不设置链接寄存器。
BLR 指令:跳转到寄存器中的地址,保存返回地址到链接寄存器(例如 x30),用于函数调用和返回。*/
// B #pc	一般是无参数 无条件分支指令 [目标:函数内部_标签] (循环,分支...)
0x0:      00 00 00 14            B 0// BL		一般是有参数 函数调用指令 [目标:导出函数_标签]
0x4:      FF FF FF 97            BL  0// BLR		 函数调用和返回指令 [目标:指定寄存器里存的地址],并且可以在调用后,返回调用的位置继续执行
0x8:      00 02 3F D6            BLR x16// BLR		 无条件地跳转 [目标:指定寄存器里存的地址],但是不能回到原来调用位置
0xc:      00 00 1F D6            BR x0// ============= [HOOK 寻址计算] ============= 
//  1.B和BL	指令		(A里面调用B函数)
size_t pc_offset = (B地址 - A里当前指令位置地址)/4;	(地址间相差多少条指令)
// 计算指令
uint_fast64_t mask = 0x03ffffffu;
long b_inst =0x14000000 | ((pc_offset) & mask);
// 使用B pc_offsetBL pc_offset// 2.BR指令,original是要HOOK的函数的指针,symbol是要HOOK的函数//uint32_t *original = static_cast<uint32_t *>(symbol);//地址写入对齐校验//奇数:地址写入内存时需要对齐内存,需要补充一个NOP指令,完成对齐//偶数:不需要处理,直接写入即可int32_t count = ((uint64_t)((uint32_t *)original + 2) & 7u) != 0u ? 5 : 4;//把地址写到相对当前位置偏移处/*** LDR X17, #0x8* [分析]* LDR X17,#距离当前位置偏移的数值* 0x4  LDR X17, #0x8* 从LDR下一行开始数,每一行是一个0x4* 从LDR下一行开始,第二行写入要跳转的函数的地址**/original[0] = 0x58000051u; // LDR X17, #0x8				(addr:0x0)//br	x17original[1] = 0xd61f0220u; // BR X17    20 02 1F D6		(addr:0x4)//写入要跳转的新函数地址original[2] = replace; // 这里对应0x8的位置				(addr:0x8)// ============= [优化处理] ============= 
int edit_count = 修改指令的个数;
// 刷新代码缓存
__flush_cache(symbol, edit_count* sizeof(uint32_t));

PLT HOOK

#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)// 解锁内存
int a = mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);printf("[状态码]:%d\n",a);// 擦除缓存__builtin___clear_cache((char *)PAGE_START(addr), (char *)PAGE_END(addr));

处理libc.so里malloc…这种函数

#include <iostream>__attribute__((constructor))
void my_init() {printf("[A64 HOOK 框架加载成功]\n");// 可以在这里执行其他初始化操作
}__attribute__((destructor()))
void my_cleanup() {printf("[A64 HOOK 框架加载成功]\n");// 可以在这里执行其他初始化操作
}

一些测试

我的ElfHOOK传说记录

#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <dlfcn.h>
#include "test.h"#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)
static void* (*real_malloc)(size_t) = NULL;void *my_malloc(size_t size)
{if (!real_malloc) {real_malloc = dlsym(RTLD_NEXT, "malloc");if (!real_malloc) {fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());exit(EXIT_FAILURE);}}printf("%zu bytes memory are allocated by libtest.so\n", size);return real_malloc(size);
}void hook()
{char       line[512];FILE      *fp;uintptr_t  base_addr = 0;uintptr_t  addr;//find base address of libtest.soif(NULL == (fp = fopen("/proc/self/maps", "r"))) return;while(fgets(line, sizeof(line), fp)){if(NULL != strstr(line, "executable") &&sscanf(line, "%" PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)break;}fclose(fp);if(0 == base_addr) return;//the absolute addressaddr = base_addr + 0x11FA8;//add write permissionmprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);//replace the function address*(void **)addr =(void *)(my_malloc);//clear instruction cache
//    __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}int main()
{hook();say_hello();say_hello();say_hello();say_hello();return 0;
}

参考

https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md

test.h

#ifndef TEST_H
#define TEST_H 1#ifdef __cplusplus
extern "C" {
#endifvoid say_hello();#ifdef __cplusplus
}
#endif#endif

tese.c

#include <stdlib.h>
#include <stdio.h>void say_hello()
{char *buf = malloc(1024);if(NULL != buf){snprintf(buf, 1024, "%s", "hello\n");printf("%s", buf);}
}

基础HOOK

#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include "test.h"#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)void *my_malloc(size_t size)
{printf("%zu bytes memory are allocated by libtest.so\n", size);return malloc(size);
}void hook()
{char       line[512];FILE      *fp;uintptr_t  base_addr = 0;uintptr_t  addr;//find base address of libtest.soif(NULL == (fp = fopen("/proc/self/maps", "r"))) return;while(fgets(line, sizeof(line), fp)){if(NULL != strstr(line, "executable") &&sscanf(line, "%" PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)break;}fclose(fp);if(0 == base_addr) return;//the absolute addressaddr = base_addr + 0x11FB0;//add write permissionmprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);//replace the function address*(void **)addr =(void *)(my_malloc);//clear instruction cache__builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}int main()
{hook();say_hello();return 0;
}

1.基于LD_LIBRARY_PATH的加载引导HOOK

原理探索

# 这是最基础的【库函数】hook方法
# 编译上面的libtest.so库  和 main
# 然后
adb push ./libtest.so ./main /data/local/tmp
adb shell "chmod +x /data/local/tmp/main"
# 关键是这里的 export LD_LIBRARY_PATH=/data/local/tmp;
# 这里是延时加载的,在main里调用的函数,会率先从/data/local/tmp/libtest.so里找,找不到的才会去libc.so这样的库里找
adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"

2.ELF的结构分析

为啥要分析这个,因为基于PLT的HOOK需要这个分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.查看基础头信息
# 命令 arm-linux-androideabi-readelf -h 你的.so
# 或者NDK下的 D:\NDK\android-ndk-r19c\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\aarch64-linux-android\bin\readelf -h 你的.so
ELF Header:Magic:   7f 45 4c 46 | 02 01 01 00 00 00 00 00 00 00 00 00  #魔术变量ELF|架构(02:64bit 01:32bit) ....   Class:                             ELF64 #类型Data:                              2's complement, little endian # 二进制补码(规范处理正负数),大小端(HEX阅读顺序)信息Version:                           1 (current) # (ELF版本号)OS/ABI:                            UNIX - System V # (架构)ABI Version:                       0 # (架构版本)Type:                              DYN (Shared object file) # (共享对象文件,需要其他库支撑运行)Machine:                           AArch64 #(AArch64 架构)Version:                           0x1 #(ELF 文件格式的版本号)Entry point address:               0xb10 #(程序开始执行的入口地址,_start_main的偏移地址,注意并非是C代码里的main函数地址)Start of program headers:          64 (bytes into file) #程序头表偏移地址0x40,可以说都是这个位置开始的Start of section headers:          14864 (bytes into file) #节头表偏移地址Flags:                             0x0 # 文件头的标志字段,这里没有设置任何特定的标志,因此为0x0。Size of this header:               64 (bytes) # ELF 文件头的大小为64 (0x40)字节Size of program headers:           56 (bytes) # 每个程序头的大小为56字节Number of program headers:         9 # 包含9个程序头表Size of section headers:           64 (bytes) # 每个节头的大小为64字节Number of section headers:         34# 包含34个节头表Section header string table index: 31 # 指向节头表字符串表的索引为31。节头表字符串表包含节名称的字符串D:\NDK\android-ndk-r19c\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\aarch64-linux-android\bin>
  	# Section header string table index  节表名字在第几个节表内存着# sh_offse + sh_size+sh_num  ->读取所有的节表 其实这个就是个动态分配的数组# Elf64_Shdr *shdrs = (Elf64_Shdr*)malloc(elf_header->e_shnum*sizeof(Elf64_Shdr));# 取出 节表名字对应的节表 -> Elf64_Shdr shstrtab = shdrs[elf_header->e_shstrndx];# 读取节表的所有内容 (其实只是个Tab标签) char *shstrtab_data =(char*) malloc(shstrtab.sh_size);# pread64(fd, shstrtab_data,shstrtab.sh_size,shstrtab.sh_offset); # 从字节表名字偏移处读取一个,读取到shstrtab_data里面

3.基于PLT的HOOK

原理探索

程序头
节表列表	Elf64_Shdr 	elf_header->e_shentsize * elf_header->e_shnum
节表名单	Elf64_Shdr	shdrs[elf_header->e_shstrndx]所有[函数]符号表
.symtab	符号列表 [函数...结构体] --- int index = symtab->st_name.strtab	符号名字列表	[函数所有名字]  char* fuc_name = strtab[index]
很多不同的内容,导入函数 addr: 0000000  Type:FUN		GLOBAL  	(可能有@@LIB)
[过滤所有导入符号]
if(sym->st_value ==0 &&(ELF64_ST_TYPE(sym->st_info) == STT_FUNC) && ELF64_ST_BIND(sym->st_info) == STB_GLOBAL)
// 判断函数名字
记录当前序号i (是addr: 0000000  Type:FUN		GLOBAL这个类型里的序号!!!!!).rela.plt	关联表	[libc.so等导入函数的结构体] ------ Elf64_Rel -----	plt_base_offset = rela_plt_tab[0].r_offset 
计算导入函数
target = plt_base_offset + 8*iHOOK #define PAGE_START(addr) ((addr) & PAGE_MASK)#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)// 计算PLT地址addr = lib_base+target// 给予可修改权限mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE)// 替换*(void **)addr =(void *)(replace);// 擦除缓存__builtin___clear_cache((char *)PAGE_START(addr), (char *)PAGE_END(addr));
Elf64_Ehdr *elf_header = (Elf64_Ehdr *) malloc(sizeof(Elf64_Ehdr));
拿到所有节表
Elf64_Shdr *shdrs = new Elf64_Shdr[elf_header->e_shnum];节表所有名字保存在 index = elf_header->e_shstrndx 这个节表里
Elf64_Shdr shstrtab = shdrs[elf_header->e_shstrndx];
拿到所有表的名字数组
char *shstrtab_data =(char*) malloc(shstrtab.sh_size);遍历所有节,获取节真实名字
shstrtab_data + shdrs[i].sh_name记录 .symtab 序号 ----------- 所有函数详细信息,偏移,类型(名字不在这里)
记录 .strtab 序号 ----------- 所有函数名字
char *dynstr = (char *)malloc(strtab.sh_size);
记录 .rela.plt 序号从strtab表获取到(系统函数的名字)并从symtab表记录这个类型的索引++[sym->st_value] 偏移量:0[ELF64_ST_TYPE(sym->st_info)] 类型: STT_FUNC[ELF64_ST_BIND(sym->st_info)] 绑定类型:STB_GLOBAL记录目标fucname 对应的0&STB_GLOBAL&STT_FUNC的	[目标GLOBAL索引序号]获取 .plt 表 包含的是Elf64_Rel重定向信息
Elf64_Shdr rela_plt_section = shdrs[rela_plt_index];
拿到第一个 .plt 表元素的偏移量
frist_global_offset = rela_plt_tab[0].r_offset;根据 [目标GLOBAL索引序号] 计算目标函数偏移
目标函数偏移 = rela_plt_tab[0].r_offset + [目标GLOBAL索引序号]*0x8--------
为什么这样计算??? 直接获取对应的 [目标GLOBAL索引序号]  取出 rela_plt_tab[index].r_offset不就行了???不行,因为可以直接遍历出来的rela_plt_tab[index]是有限个的,不一定会真的获取到所需要的函数也就是 [系统函数] 用了10,但是 .plt 里看到的,可能只有5个或者更少,所以要计算--------
获取后 uintptr_t addr  = info.lib_base + info.target_offset;mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);---------#include <inttypes.h>#define PAGE_SIZE 4096#define PAGE_MASK (~(PAGE_SIZE - 1))---------#define PAGE_START(addr) ((addr) & PAGE_MASK) // 内存对齐#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)*(void **)addr =(void *)(replace); // 替换函数__builtin___clear_cache((char *)PAGE_START(addr), (char *)PAGE_END(addr)); //擦除原函数的缓存

处理libc.so里malloc…这种函数

汇编HOOK的研究

#include <iostream>
#include <cstdio>
#include <cstring>
#include <inttypes.h>
#include <asm-generic/fcntl.h>
#include <fcntl.h>
#include <linux/elf.h>
#include <unistd.h>
#include <elf.h>
#include <asm-generic/mman.h>
#include <sys/mman.h>
#include <dlfcn.h>
#include "Hook/PltHook.h"
#include "Hook/IHOOK.hpp"
#include "Hook/IASM64.h"
#include "Hook/And64InlineHook.hpp"/*** inline HOOK 2种* 1.当前so内直接调用 ----- 1* 前后 64 MB* B  #pc* 2.调用其他so内函数 ----- 6* STP x8,x0 ,[sp-20]* LDR X0,8* BR X0* [ADDR]* LDR X8,[sp]* @return*/int inline_A() {printf("[本so] 函数A\n");return 666;
}void inline_B() {printf("[本so] HOOK 到了 函数B\n");
}// 跳板子
int (*inline_A_tamp)() = NULL;static __attribute((aligned(PAGE_SIZE))) uint32_t __insns_pool[256][5 * 10];// 跳板集合void log(char* tag,ADDR_TYPE value){printf("[%s] %llx\n",tag,value);
}#define __flush_cache(c, n)        __builtin___clear_cache(reinterpret_cast<char *>(c), reinterpret_cast<char *>(c) + n)void HOOK_inline(const void *symbol, void *replace, void **result) {// 创建一个跳板 池子,每次 HOOK都会 在池子里选择一个格子 256次HOOK 每次HOOK里可以存40条指令// 最好这样写static __attribute((aligned(PAGE_SIZE))) uint32_t __insns_pool[256][4 * 10];//    static uint32_t __insns_pool[256][5 * 10];// HOOK 次数计数器static __attribute((aligned(PAGE_SIZE))) uint32_t tams_index = 0;// 计算pc偏移INST_IMPORT pc_offset = getPC_ADDR(ADDR_TYPE(symbol), ADDR_TYPE(replace));printf("[pc] %llx\n",pc_offset );// 如果在+-64MB范围之内,直接使用B跳转if (llabs(pc_offset) <= (B_PC_MASK >> 1)) {// 一般是在自己程序调试时使用printf("hook small\n");// 回调原函数if (result != NULL) {// 开启当前备份 rwxp -> 可读可写可执行 权限mwrxp(FUN_CALL(__insns_pool[tams_index]), 1);// 获取原函数第一条指令,用作备份INST_TYPE raw_frist = getasm32(FUN_CALL(symbol), 0);INST_IMPORT backup_pc_offset = getPC_ADDR(ADDR_TYPE(__insns_pool[tams_index]), ADDR_TYPE(symbol));__insns_pool[tams_index][0] = raw_frist; // 第一条是原指令__insns_pool[tams_index][1] = backup_pc_offset; // B symbol+4 # 调到原函数的第二条指令继续执行*result = __insns_pool[tams_index]; // 把当前指令配置给回调函数tams_index++;   // 更新池子索引}// 开启原函数的      rwxp ->可写权限 权限mwrxp(FUN_CALL(symbol), 1);// 替换原函数第一条指令为 B #replace函数editasm32(FUN_CALL(symbol), 0, pc_offset);} else {// 一般是在自己程序---加载,引用库时,启用printf("hook big\n");int32_t count = ((uint64_t)((uint32_t *)symbol + 2) & 7u) != 0u ? 5 : 4;// 开启原函数的      rwxp ->可写权限 权限mwrxp(FUN_CALL(symbol), 1);if (count == 5){uint32_t *original = (uint32_t *)symbol; // symbol要HOOK的函数editasm32(FUN_CALL(symbol), 1*0x4, a64_ldr_x_num(17,0x8)); // LDR X17, #0x8editasm32(FUN_CALL(symbol), 2*0x4, a64_br_x(17)); // BR X17editasm64(FUN_CALL(symbol), 3*0x4, replace); // BR X17__flush_cache((char*)symbol, 5 * sizeof(uint32_t));}//        __flush_cache(symbol, 7* sizeof(uint32_t));}}void BB() {printf("[本so] BB\n");
}int (*AA)() = NULL;int (*CC)() = NULL;int test_inline_hook_local() {// 总共6条指令printf("----------[调用原A]-----------\n");inline_A();//    A64HookFunction(FUN_CALL(inline_A), FUN_CALL(inline_B), (void **) &AA);
//    A64HookFunction(FUN_CALL(AA), FUN_CALL(BB), (void **) &CC);
//    lookInst(ADDR_TYPE(AA),30);HOOK_inline(FUN_CALL(inline_A), FUN_CALL(inline_B), (void **) &AA);HOOK_inline(FUN_CALL(AA), FUN_CALL(BB), (void **) &CC);printf("----------[A被HOOK后  调用A]-----------\n");inline_A();if (AA) {printf("----------[调用AA]-----------\n");AA();}printf("----------[AA被HOOK后]-----------\n");AA();if (CC) {CC();printf("[调用CC]\n");}return 0;
}#define 程序入口 main
void (*function_in_file2)() = NULL;int 程序入口() {char* path = "/data/local/tmp/libts_arm64-v8a.so";void *lib_handle = dlopen(path,RTLD_LAZY);if (!lib_handle) {fprintf(stderr, "Failed to open library: %s\n", dlerror());return -1;  // 处理打开失败的情况}printf("\033[37;3m 基础 HOOK 框架 \033[0m\n");function_in_file2 = (void (*)()) dlsym(lib_handle,"_Z21Dysm_shared_hook_Testv");printf(" 【库】0x%llx\n",  function_in_file2);printf("【自己】0x%llx\n",  inline_A);printf("----------[调用原hook_Test]-----------\n");function_in_file2();void* symbol = (void*)function_in_file2;void* replace = (void*)inline_A;void** results = (void**)AA;//    A64HookFunction(FUN_CALL(symbol), FUN_CALL(replace), FUN_CALL_RAW(&results));int32_t count = ((uint64_t)((uint32_t *)symbol + 2) & 7u) != 0u ? 5 : 4;printf("[PC] 0x%llx\n",count);mwrxp(symbol,1);uint32_t *original = (uint32_t *)symbol; // symbol要HOOK的函数HOOK_inline(FUN_CALL(symbol), FUN_CALL(replace), FUN_CALL_RAW(&results));printf("----------[HOOK后  调用原hook_Test]-----------\n");function_in_file2();printf("----------[结束]-----------\n");return 0;
}void iii(){uint64_t obb = 0b11110001101010010001101111111010001100011110100;size_t s = 30;for (int i = 0; i < s; ++i){printf("[%d]  %llx\n",i,  obb&i);}}

结束

可能比较乱,没办法,这几年Hook的个人研究的一部分,本来准备早点发的,但是由于CSDN上传图片总是失败,一张一张上传太麻烦,所以直接不上传图了,其它文件太多,也不想发出来

不要提问,因为好久不整了,也不打算整了,不做回复请见谅 > _ <


http://www.ppmy.cn/server/158322.html

相关文章

NGINX--HTTPTCP负载均衡

一、HTTP负载均衡 1、基本介绍 在多应用实例中&#xff0c;通常可以用nginx来做负载均衡器来分发流量&#xff0c;以达到提高应用吞吐量、降低时延、调优性能、提供容错性等。 2、http负载均衡最简单的配置如下 http {upstream myapp1 {server srv1.example.com;server srv…

阿里云直播Web

官方文档&#xff1a;Web播放器SDK常见问题_视频点播(VOD)-阿里云帮助中心 bug&#xff1a;播流的不稳定&#xff0c;直播总会进入 onM3u8Retry 监听&#xff0c;用户端就会黑屏&#xff0c;&#xff08;但其实并没有关播&#xff0c;正常关播进入的是pause这个监听&#xff0…

IP 地址与蜜罐技术

基于IP的地址的蜜罐技术是一种主动防御策略&#xff0c;它能够通过在网络上布置的一些看似正常没问题的IP地址来吸引恶意者的注意&#xff0c;将恶意者引导到预先布置好的伪装的目标之中。 如何实现蜜罐技术 当恶意攻击者在网络中四处扫描&#xff0c;寻找可入侵的目标时&…

【新人系列】Python 入门(二十六):常见设计模式

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

一文清晰梳理Mysql 数据库

现在处于大四上学期的阶段&#xff0c;在大四下学期即将要进行毕业设计&#xff0c;所以在毕业设计开始之前呢&#xff0c;先将Mysql 数据库有关知识进行了一个梳理&#xff0c;以防选题需要使用到数据库。 1&#xff09;什么是数据库&#xff1f; 简单理解数据库&#xff0c…

torch.einsum计算张量的外积

torch.einsum 是一种强大的张量操作工具,可以通过爱因斯坦求和约定(Einstein summation convention)来简洁地表示复杂的张量运算。通过它,我们可以高效地计算矩阵乘法、转置、点积、外积等操作。 以下是关于如何使用 torch.einsum 计算两个四维张量在第三维度上的外积的解…

若依前后端分离项目部署

一、后端 1.修改文件存储路径 2.修改后端端口号&#xff0c;也可不进行修改&#xff08;端口号没有被占用&#xff09; 3.修改redis的地址&#xff08;云服务器地址&#xff09;、端口号以及密码 4.修改MySQL数据库的地址以及密码 5.修改日志存储路径 6.打包 7.将jar包上传到服…

Spring Boot 2 学习全攻略

Spring Boot 2 学习资料 Spring Boot 2 学习资料 Spring Boot 2 学习资料 在当今快速发展的 Java 后端开发领域&#xff0c;Spring Boot 2 已然成为一股不可忽视的强大力量。它简化了 Spring 应用的初始搭建以及开发过程&#xff0c;让开发者能够更加专注于业务逻辑的实现&am…