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

Linux arm64 set_memory_ro/rw函数

文章目录

  • 一、函数简介
    • 1.1 简介
    • 1.2 change_memory_common
    • 1.3 __change_memory_common
  • 二、apply_to_page_range函数
    • 2.1 apply_to_page_range
    • 2.2 apply_to_p4d_range
    • 2.3 apply_to_pud_range
    • 2.4 apply_to_pmd_range
    • 2.5 apply_to_pte_range
  • 三、hook系统调用
  • 参考资料

一、函数简介

1.1 简介

// linux-5.4.18/arch/arm64/mm/pageattr.cint set_memory_ro(unsigned long addr, int numpages)
{return change_memory_common(addr, numpages,__pgprot(PTE_RDONLY),__pgprot(PTE_WRITE));
}int set_memory_rw(unsigned long addr, int numpages)
{return change_memory_common(addr, numpages,__pgprot(PTE_WRITE),__pgprot(PTE_RDONLY));
}

arm64架构下 set_memory_ro 和 set_memory_rw 用来更改内核内存区域的可读可写属性,只用于更改由 vmalloc 和 vmap分配的内存区间。 两者都是调用 change_memory_common 函数。

1.2 change_memory_common

static int change_memory_common(unsigned long addr, int numpages,pgprot_t set_mask, pgprot_t clear_mask)
{unsigned long start = addr;//计算要更改内存地址的大小unsigned long size = PAGE_SIZE*numpages;unsigned long end = start + size;struct vm_struct *area;int i;//检查addr是否未对齐到页面边界。如果未对齐,则将start地址调整为前一个页面边界,并相应地更新endif (!PAGE_ALIGNED(addr)) {start &= PAGE_MASK;end = start + size;WARN_ON_ONCE(1);}/** Kernel VA mappings are always live, and splitting live section* mappings into page mappings may cause TLB conflicts. This means* we have to ensure that changing the permission bits of the range* we are operating on does not result in such splitting.** Let's restrict ourselves to mappings created by vmalloc (or vmap).* Those are guaranteed to consist entirely of page mappings, and* splitting is never needed.** So check whether the [addr, addr + size) interval is entirely* covered by precisely one VM area that has the VM_ALLOC flag set.*///来查找覆盖指定地址范围的虚拟内存区域area,area由vmalloc or vmap 分配area = find_vm_area((void *)addr);//如果找不到这样的区域,或者范围的结束地址超过了区域的边界,或者该区域没有设置VM_ALLOC标志,函数将返回错误代码-EINVALif (!area ||end > (unsigned long)area->addr + area->size ||!(area->flags & VM_ALLOC))return -EINVAL;if (!numpages)return 0;/** If we are manipulating read-only permissions, apply the same* change to the linear mapping of the pages that back this VM area.*///我手中机器配置没有rodata_full标志if (rodata_full && (pgprot_val(set_mask) == PTE_RDONLY ||pgprot_val(clear_mask) == PTE_RDONLY)) {for (i = 0; i < area->nr_pages; i++) {__change_memory_common((u64)page_address(area->pages[i]),PAGE_SIZE, set_mask, clear_mask);}}/** Get rid of potentially aliasing lazily unmapped vm areas that may* have permissions set that deviate from the ones we are setting here.*///以删除任何可能存在的与当前函数中设置的权限不同的潜在别名延迟取消映射的VM区域vm_unmap_aliases();//调用__change_memory_common,传入调整后的start地址、大小和set_mask、clear_mask,以对指定的内存范围应用权限更改return __change_memory_common(start, size, set_mask, clear_mask);
}

change_memory_common函数是一个用于更改指定内存页面范围权限。

函数change_memory_common接受四个参数:

addr:内存范围的起始地址。
numpages:内存范围中的页面数量。
set_mask:要设置的保护掩码,表示要设置的权限位。
clear_mask:要清除的保护掩码,表示要清除的权限位。

函数首先根据地址addr计算起始地址start和结束地址end,并计算需要更改权限的内存大小size。如果起始地址addr不是页对齐的,会将start调整为页对齐,并重新计算end。同时,会发出一个警告(WARN_ON_ONCE(1))。

接下来,函数检查要操作的内存范围是否完全位于一个由vmalloc(或vmap)创建的虚拟内存区域中。函数调用find_vm_area来查找覆盖指定地址范围的虚拟内存区域。如果找不到这样的区域,或者范围的结束地址超过了区域的边界,或者该区域没有设置VM_ALLOC标志,函数将返回错误代码-EINVAL。这个检查确保指定范围完全被一个具有VM_ALLOC标志的VM区域覆盖,该标志表示该区域完全由页面映射组成,不需要进行分割。

如果numpages为0,表示没有需要更改权限的页,直接返回0。

如果设置了rodata_full标志,并且set_mask或clear_mask指示了只读权限(PTE_RDONLY),函数将遍历VM区域的页面,并调用__change_memory_common,将相同的权限更改应用于页面的线性映射。这一步确保相应的线性映射权限与VM区域权限保持一致。

我手中机器配置没有rodata_full标志:

# CONFIG_RODATA_FULL_DEFAULT_ENABLED is not set
bool rodata_full __ro_after_init = IS_ENABLED(CONFIG_RODATA_FULL_DEFAULT_ENABLED);

在对指定的内存范围应用权限更改之前,函数调用vm_unmap_aliases,以删除任何可能存在的与当前函数中设置的权限不同的潜在别名延迟取消映射的VM区域。

最后,函数调用__change_memory_common,传入调整后的start地址、大小和set_mask、clear_mask,以对指定的内存范围应用权限更改。

1.3 __change_memory_common

/** This function assumes that the range is mapped with PAGE_SIZE pages.*/
static int __change_memory_common(unsigned long start, unsigned long size,pgprot_t set_mask, pgprot_t clear_mask)
{//page_change_data结构体用于存储设置和清除的保护掩码struct page_change_data data;int ret;data.set_mask = set_mask;data.clear_mask = clear_mask;ret = apply_to_page_range(&init_mm, start, size, change_page_range,&data);flush_tlb_kernel_range(start, start + size);return ret;
}

__change_memory_common的函数,用于更改指定内存页面范围的内存权限。

函数__change_memory_common接受四个参数:

start:内存范围的起始地址。
size:内存范围的大小(以字节为单位)。
set_mask:要设置的保护掩码,表示要设置的权限位。
clear_mask:要清除的保护掩码,表示要清除的权限位。

函数调用apply_to_page_range,对init_mm(内核初始化的内存描述符)中指定的内存范围应用给定的函数change_page_range。change_page_range函数被用于处理页范围内的每个页面,并根据data中的保护掩码设置和清除页面的权限。apply_to_page_range函数返回一个整数值,表示操作的结果。

struct page_change_data {pgprot_t set_mask;pgprot_t clear_mask;
};static int change_page_range(pte_t *ptep, unsigned long addr, void *data)
{struct page_change_data *cdata = data;pte_t pte = READ_ONCE(*ptep);pte = clear_pte_bit(pte, cdata->clear_mask);pte = set_pte_bit(pte, cdata->set_mask);set_pte(ptep, pte);return 0;
}

名为change_page_range的函数,作为apply_to_page_range函数的参数,函数使用clear_pte_bit宏和set_pte_bit宏分别对pte进行清除和设置操作,根据cdata中的保护掩码指定的权限位。这些宏用于修改页表项的权限位,以实现更改页面权限的目的。

函数调用flush_tlb_kernel_range来刷新内核页表中指定范围的TLB(转换查找缓冲器)。TLB是用于加速虚拟地址到物理地址转换的硬件缓存。刷新TLB确保最新的内存权限设置生效。

二、apply_to_page_range函数

2.1 apply_to_page_range

// linux-5.4.18/mm/memory.c/** Scan a region of virtual memory, filling in page tables as necessary* and calling a provided function on each leaf page table.*/
int apply_to_page_range(struct mm_struct *mm, unsigned long addr,unsigned long size, pte_fn_t fn, void *data)
{pgd_t *pgd;unsigned long next;unsigned long end = addr + size;int err;if (WARN_ON(addr >= end))return -EINVAL;pgd = pgd_offset(mm, addr);do {next = pgd_addr_end(addr, end);err = apply_to_p4d_range(mm, pgd, addr, next, fn, data);if (err)break;} while (pgd++, addr = next, addr != end);return err;
}
EXPORT_SYMBOL_GPL(apply_to_page_range);

pply_to_page_range的函数,用于扫描虚拟内存的一个区域,并在需要时填充页表,同时在每个叶子页表上调用提供的函数。在这里我们提供的函数是change_page_range,修改page属性。

函数apply_to_page_range接受五个参数:

mm:指向mm_struct结构体的指针,表示进程的内存描述符。 -- 这里传递的是 init_mm
addr:内存范围的起始地址。
size:内存范围的大小(以字节为单位)。
fn:指向函数的指针,该函数将在每个叶子页表上调用。 -- 这里传递的是change_page_range
data:传递给fn函数的数据。

(1)通过调用pgd_offset函数,根据给定的进程内存描述符mm和起始地址addr获取页全局目录项(PGD)的指针,并将其存储在pgd中。

struct mm_struct init_mm = {.pgd		= swapper_pg_dir,
};

这里页全局目录项(PGD)的指针就是指内核页表页全局目录项swapper_pg_dir。

(2)使用一个循环来遍历地址范围内的每个页全局目录项。在每次迭代中,通过调用pgd_addr_end函数计算下一个要处理的地址next,根据当前的addr和end。

/** When walking page tables, get the address of the next boundary,* or the end address of the range if that comes earlier.  Although no* vma end wraps to 0, rounded up __boundary may wrap to 0 throughout.*/#define pgd_addr_end(addr, end)						\
({	unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;	\(__boundary - 1 < (end) - 1)? __boundary: (end);		\
})

该宏用于计算页全局目录项(PGD)的下一个边界地址。它接受两个参数,addr表示当前地址,end表示范围的结束地址。宏的工作如下:

首先,它将当前地址addr加上页全局目录的大小(PGDIR_SIZE),然后与页全局目录的掩码(PGDIR_MASK)进行按位与操作,得到一个边界地址__boundary。

接下来,宏检查(__boundary - 1 < (end) - 1)是否为真。这里通过使用减一操作来避免了可能发生的边界溢出问题。

如果条件为真,则返回__boundary,否则返回end。

(4)然后,调用apply_to_p4d_range函数,传递进程内存描述符mm、当前的页全局目录项pgd、当前地址addr、下一个地址next、提供的函数fn和数据data。该函数的作用是在页全局目录项范围内递归地处理页中间目录项(P4D)。

2.2 apply_to_p4d_range

static int apply_to_p4d_range(struct mm_struct *mm, pgd_t *pgd,unsigned long addr, unsigned long end,pte_fn_t fn, void *data)
{p4d_t *p4d;unsigned long next;int err;p4d = p4d_alloc(mm, pgd, addr);if (!p4d)return -ENOMEM;do {next = p4d_addr_end(addr, end);err = apply_to_pud_range(mm, p4d, addr, next, fn, data);if (err)break;} while (p4d++, addr = next, addr != end);return err;
}

arm64最大为四级页表,p4d一般只有x86_64架构下才有,这里不予讨论。

2.3 apply_to_pud_range

static int apply_to_pud_range(struct mm_struct *mm, p4d_t *p4d,unsigned long addr, unsigned long end,pte_fn_t fn, void *data)
{pud_t *pud;unsigned long next;int err;pud = pud_alloc(mm, p4d, addr);if (!pud)return -ENOMEM;do {next = pud_addr_end(addr, end);err = apply_to_pmd_range(mm, pud, addr, next, fn, data);if (err)break;} while (pud++, addr = next, addr != end);return err;
}

函数的执行流程如下:
首先,通过调用pud_alloc函数为给定的P4D分配内存空间,并将返回的PUD指针赋值给pud变量。

进入一个循环,该循环将在PUD范围内进行迭代处理。

在循环的每一次迭代中,首先调用pud_addr_end宏来计算下一个边界地址next。这个宏将根据当前地址addr和范围结束地址end计算出下一个PMD(Page Middle Directory)的边界地址。

然后,调用apply_to_pmd_range函数来对PMD范围内的页面应用函数fn。该函数将处理mm、pud、addr和next之间的页面,并使用fn函数进行处理。

如果没有出现错误,继续进行下一次迭代。在每次迭代中,通过递增pud指针、更新addr为next,并检查addr是否等于end来确定是否继续循环。

当addr等于end时,表示已经处理完整个范围。

这段代码的作用是在给定的PUD范围内对页面应用函数进行处理,通过逐个PMD范围进行迭代,并在每个PMD范围内调用给定的函数进行处理。

2.4 apply_to_pmd_range

static int apply_to_pmd_range(struct mm_struct *mm, pud_t *pud,unsigned long addr, unsigned long end,pte_fn_t fn, void *data)
{pmd_t *pmd;unsigned long next;int err;BUG_ON(pud_huge(*pud));pmd = pmd_alloc(mm, pud, addr);if (!pmd)return -ENOMEM;do {next = pmd_addr_end(addr, end);err = apply_to_pte_range(mm, pmd, addr, next, fn, data);if (err)break;} while (pmd++, addr = next, addr != end);return err;
}

函数的执行流程如下:
首先,使用BUG_ON宏检查给定的PUD是否是巨页(huge page)。如果是巨页,会引发一个bug检查(bug-on),表示代码中出现了不应该出现的情况。

调用pmd_alloc函数为给定的PUD分配一个PMD,并将返回的PMD指针赋值给pmd变量。如果内存分配失败,函数将返回错误码-ENOMEM。

进入一个循环,该循环将在PMD范围内进行迭代处理。

在循环的每一次迭代中,首先调用pmd_addr_end宏来计算下一个边界地址next。这个宏将根据当前地址addr和范围结束地址end计算出下一个PTE(Page Table Entry)的边界地址。

然后,调用apply_to_pte_range函数来对PTE范围内的页面应用函数fn。该函数将处理mm、pmd、addr和next之间的页面,并使用fn函数进行处理。如果在处理过程中出现错误,将返回一个非零的错误码err。

如果err不为零,表示在处理过程中出现了错误,函数将跳出循环。

如果没有出现错误,继续进行下一次迭代。在每次迭代中,通过递增pmd指针、更新addr为next,并检查addr是否等于end来确定是否继续循环。

当addr等于end时,表示已经处理完整个范围,函数将返回err。

这段代码的作用是在给定的PMD范围内对页面应用函数进行处理,通过逐个PTE范围进行迭代,并在每个PTE范围内调用给定的函数进行处理。

2.5 apply_to_pte_range

static int apply_to_pte_range(struct mm_struct *mm, pmd_t *pmd,unsigned long addr, unsigned long end,pte_fn_t fn, void *data)
{pte_t *pte;int err;spinlock_t *uninitialized_var(ptl);pte = (mm == &init_mm) ?pte_alloc_kernel(pmd, addr) :pte_alloc_map_lock(mm, pmd, addr, &ptl);if (!pte)return -ENOMEM;BUG_ON(pmd_huge(*pmd));arch_enter_lazy_mmu_mode();do {err = fn(pte++, addr, data);if (err)break;} while (addr += PAGE_SIZE, addr != end);arch_leave_lazy_mmu_mode();if (mm != &init_mm)pte_unmap_unlock(pte-1, ptl);return err;
}

函数的执行流程如下:

根据mm是否等于&init_mm,选择不同的方式分配PTE。如果mm等于&init_mm,表示当前是内核线程,使用pte_alloc_kernel函数为给定的PMD和地址分配一个PTE;否则,使用pte_alloc_map_lock函数为给定的进程mm、PMD和地址分配一个PTE,并将分配过程中获取的自旋锁地址保存在ptl中。如果无法分配PTE,则返回错误码-ENOMEM。

使用BUG_ON宏检查给定的PMD是否是巨页(huge page)。如果是巨页,会引发一个bug检查(bug-on),表示代码中出现了不应该出现的情况。

调用arch_enter_lazy_mmu_mode函数,进入延迟MMU模式。这个函数是架构相关的,用于在某些架构上进入延迟更新MMU页表的模式。

进入一个循环,该循环将在PTE范围内进行迭代处理。

在循环的每一次迭代中,首先调用给定的函数fn来处理当前的PTE,传递pte、addr和data作为参数。如果在处理过程中出现错误,将返回一个非零的错误码err。

如果err不为零,表示在处理过程中出现了错误,函数将跳出循环。

如果没有出现错误,继续进行下一次迭代。在每次迭代中,通过递增pte指针、更新addr为addr + PAGE_SIZE,并检查addr是否等于end来确定是否继续循环。

完成PTE范围的处理后,调用arch_leave_lazy_mmu_mode函数,离开延迟MMU模式。

如果mm不等于&init_mm,表示当前是用户进程,调用pte_unmap_unlock函数来解除之前映射的PTE页框,并释放自旋锁。这个函数用于解除映射并解锁页表。

三、hook系统调用

set_memory_ro/rw函数只用于更改由 vmalloc 和 vmap分配的内存区间,而系统调用表不是由vmalloc 或者vmap分配的,系统调用表位于内核的只读数据区,在arm64架构中,在Linux 4.6 中 将内核镜像移到vmalloc的区域了,虽然不是由vmalloc 或者vmap分配的,但是是在vmalloc区间,如下所示:

// linux-5.4.18/arch/arm64/mm/mmu.cstatic void __init map_kernel_segment(pgd_t *pgdp, void *va_start, void *va_end,pgprot_t prot, struct vm_struct *vma,int flags, unsigned long vm_flags)
{phys_addr_t pa_start = __pa_symbol(va_start);unsigned long size = va_end - va_start;BUG_ON(!PAGE_ALIGNED(pa_start));BUG_ON(!PAGE_ALIGNED(size));__create_pgd_mapping(pgdp, pa_start, (unsigned long)va_start, size, prot,early_pgtable_alloc, flags);if (!(vm_flags & VM_NO_GUARD))size += PAGE_SIZE;vma->addr	= va_start;vma->phys_addr	= pa_start;vma->size	= size;vma->flags	= VM_MAP | vm_flags;vma->caller	= __builtin_return_address(0);//添加到vmalloc区间vm_area_add_early(vma);
}

而set_memory_ro/rw函数在调用change_memory_common函数时,只是判断内核地址是否在 vmap_area – kernel virtual area 区间,然后检查是否有VM_ALLOC标志,因此我们还是可以通过set_memory_ro/rw函数来更改系统调用表的页表属性。

static int change_memory_common(unsigned long addr, int numpages,pgprot_t set_mask, pgprot_t clear_mask)
{struct vm_struct *area;...../** Let's restrict ourselves to mappings created by vmalloc (or vmap).* Those are guaranteed to consist entirely of page mappings, and* splitting is never needed.** So check whether the [addr, addr + size) interval is entirely* covered by precisely one VM area that has the VM_ALLOC flag set.*/area = find_vm_area((void *)addr);if (!area ||end > (unsigned long)area->addr + area->size ||!(area->flags & VM_ALLOC))return -EINVAL;....../** Get rid of potentially aliasing lazily unmapped vm areas that may* have permissions set that deviate from the ones we are setting here.*/vm_unmap_aliases();return __change_memory_common(start, size, set_mask, clear_mask);
}

这样我们可以自己给该地址的vmap_area区间加上VM_ALLOC标志即可,内核镜像是在kernel virtual area 区间的,如下所示:

    area = my_find_vm_area((void *)addr);if(!area){printk("no find vm area\n");return -1;}area->flags |= VM_ALLOC;

完整的hook代码如下:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kallsyms.h> 
#include <linux/syscalls.h>
#include <linux/vmalloc.h>
#include <asm/unistd.h>
#include <asm/ptrace.h> int (*my_set_memory_ro)(unsigned long addr, int numpages);
int (*my_set_memory_rw)(unsigned long addr, int numpages);struct vm_struct *(*my_find_vm_area)(const void *addr);static unsigned long *__sys_call_table;typedef long (*syscall_fn_t)(const struct pt_regs *regs);#ifndef __NR_mkdirat
#define __NR_mkdirat 34
#endif//用于保存原始的 mkdir 系统调用
static syscall_fn_t orig_mkdir;asmlinkage long mkdir_hook(const struct pt_regs *regs)
{printk("hook mkdir sys_call\n");// return orig_mkdir(regs);return 0;
}static unsigned long addr;static int __init lkm_init(void)
{struct vm_struct *area;my_set_memory_ro = (void *)kallsyms_lookup_name("set_memory_ro");my_set_memory_rw = (void *)kallsyms_lookup_name("set_memory_rw");my_find_vm_area = (void *)kallsyms_lookup_name("find_vm_area");__sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");printk("__sys_call_table = %lx\n", __sys_call_table);//保存原始的系统调用:mkdirorig_mkdir = (syscall_fn_t)__sys_call_table[__NR_mkdirat];addr = (unsigned long)(__sys_call_table + __NR_mkdirat);addr &= PAGE_MASK;area = my_find_vm_area((void *)addr);if(!area){printk("no find vm area\n");return -1;}area->flags |= VM_ALLOC;printk("area->addr = %p, area->size = %lx\n", area->addr, area->size);    //hook 系统调用表表项:sys_call_table[__NR_mkdirat]my_set_memory_rw(addr, 1);__sys_call_table[__NR_mkdirat] = (unsigned long)mkdir_hook;my_set_memory_ro(addr, 1);printk("lkm_init\n");return 0;
}static void __exit lkm_exit(void)
{//模块卸载时恢复原来的mkdir系统调用my_set_memory_rw(addr, 1);__sys_call_table[__NR_mkdirat] = (unsigned long)orig_mkdir;my_set_memory_ro(addr, 1);printk("lkm_exit\n");
}module_init(lkm_init);
module_exit(lkm_exit);MODULE_LICENSE("GPL");

参考资料

Linux 5.4.18

相关文章:

Linux arm64 set_memory_ro/rw函数

文章目录 一、函数简介1.1 简介1.2 change_memory_common1.3 __change_memory_common 二、apply_to_page_range函数2.1 apply_to_page_range2.2 apply_to_p4d_range2.3 apply_to_pud_range2.4 apply_to_pmd_range2.5 apply_to_pte_range 三、hook系统调用参考资料 一、函数简介…...

安达发|APS排单软件中甘特图的应用

近几年来&#xff0c;企业对生产效率和管理水平的要求越来越高。为了提高生产效率&#xff0c;降低生产成本&#xff0c;许多企业开始引入先进的生产计划与调度系统&#xff08;APS&#xff09;&#xff0c;实现生产过程的自动化、智能化管理。APS排产软件是一种能够根据企业的…...

快速上手Linux基础开发工具

目录 软件包管理器 概念理解 用法示例 - 以yum为例 vim 模式的切换 常用操作 插件和配置 gcc/g gdb make / makefile 软件包管理器 概念理解 在Linux下安装软件的话&#xff0c;一个比较原始的办法是下载程序的源代码&#xff0c;然后进行编译&#xff0c;进而得到…...

【开发工具】idea 的全局搜索快捷键(Ctrl+shift+F)失效

文章目录 前言1. 取消 输入法的快捷键&#xff08;推荐使用&#xff09;2.更改 idea的快捷键3. 热键占用总结 前言 当你发现在idea 中看到用于全局搜索的快捷键就是 CtrlshiftF&#xff0c;可是怎么按都不管用的时候&#xff0c;你就不要再执着于自己的操作继续狂点电脑按键了…...

港联证券:“火箭蛋”来袭 蛋价涨势能否延续?

上个交易周&#xff08;9月11日至15日&#xff09;&#xff0c;鸡蛋期货商场呈现了意想不到的涨势。9月15日&#xff0c;鸡蛋期货多个合约大涨&#xff0c;其中2310合约涨超5.6%&#xff0c;主力合约2311盘中两度触及涨停&#xff0c;最终收涨6%。业内人士以为&#xff0c;鸡蛋…...

Vue3_vite

使用Vue-cli创建 使用vite创建 Composition API 组合API setup 1.Vue3中的一个新的配置项,值为一个函数 2.可以将组件中所用到的数据,方法等配置在setup中. 3.setup函数的两种返回值 3.1若返回一个对象,则对象中的属性,方法,在模板中均可以直接使用. 3.2若返回一个渲染函数…...

python-字符串去掉空格的常见方法

python提供了去掉字符串空格的方法&#xff0c;可以满足大部分需求。 但在实际应用中&#xff0c;还需要灵活借助python其他方法&#xff0c;来实现字符串空格的删除。 比如&#xff0c;去掉字符串的全部空格、字符串连续空格保留一个等&#xff0c;都需要结合其他的方法来实现…...

如何写出一个成熟的线上线下结合的营销方案?

分享一下咱们案例库里策划的一个线上线下结合的活动的案例。 这个活动是为了推广一个新品牌&#xff0c;增加品牌知名度和用户粘性。 你可以根据以下几个要点来进行活动策划&#xff1a; 1、目标&#xff1a; 让目标用户了解并喜欢新品牌&#xff0c;激发用户参与和分享&am…...

Vc - Qt - “扩张“的窗口

该示例演示了一个"扩张的窗口"&#xff0c;主窗口的布局为水平布局&#xff0c;内置两个子窗口&#xff0c;采用定时器设置左边窗口的宽度&#xff0c;达到控制"扩张"的目的。 #include <QApplication> #include <QWidget> #include <QHBox…...

vue学习-02vue入门之组件

删除Vue-cli预设 在用户根目录下(C:\Users\你的用户名)这个地址里有一个.vuerc 文件,修改或删除配置 组件 Props(组件之间的数据传递) Prop 的大小写 (camelCase vs kebab-case)不敏感Prop 类型: String Number Boolean Array Object Date Function Symbol传递静态或动态 Pr…...

解决Pycharm使用Conda激活环境失败的问题

Q:公司电脑终端使用powershell来激活conda环境时报错? 同时手动打开powershell报"profile.ps1” 无法被加载的错误 A: 1,手动打开powershell&#xff0c;设置管理员打开 2,打开powershell 打开 PowerShell 终端&#xff0c;并输入以下命令&#xff1a;Get-ExecutionPo…...

SpringSecurity 核心组件

文章目录 SpringSecurity 结构组件&#xff1a;SecurityContextHolder组件&#xff1a;Authentication组件&#xff1a;UserDetailsService组件&#xff1a;GrantedAuthority组件总结 SpringSecurity 结构 在SpringSecurity中的jar分为4个&#xff0c;作用分别为 jar作用spri…...

【Vue】快速入门和生命周期

目录 前言 一、vue的介绍 1. Vue.js是什么&#xff1f; 2. 库和框架的区别 3.基本概念和用法&#xff1a; 二、MVVM的介绍 1. 什么是MVVM&#xff1f; 2. MVVM的组成部分 3. MVVM的工作流程 4. MVVM的优势 5. MVVM的应用场景 三、vue实例 1.模板语法&#xff1a; …...

JVM架构和内存管理优化

Java虚拟机&#xff08;JVM&#xff09;是Java编程语言的核心组件&#xff0c;负责执行Java字节码并提供运行时环境&#xff0c;使得Java程序可以在不同的平台上运行。了解JVM的工作原理和内存管理对于优化代码性能和理解Java的内存管理和垃圾收集机制非常重要。在本文中&#…...

C语言——贪吃蛇小游戏

目录 一、ncurse 1.1 为什么需要用ncurse&#xff1a; 1.2 ncurse的输入输出&#xff1a; 1.2.1 如何使用ncurse&#xff1a; 1.2.2 编译ncurse的程序&#xff1a; 1.2.3 测试输入一个按键ncurse的响应速度&#xff1a; 1.3 ncurse上下左右键获取&#xff1a; 1.3.1 如…...

PHP8中获取并删除数组中第一个元素-PHP8知识详解

我在上一节关于数组的教程&#xff0c;讲的是在php8中获取并删除数组中最后一个元素&#xff0c;今天分享的是相反的&#xff1a;PHP8中获取并删除数组中第一个元素。 回顾一下昨天的知识&#xff0c;array_pop()函数将返回数组的最后一个元素&#xff0c;今天学习的是使用arr…...

EtherCAT 总线型 4 轴电机控制卡解决方案

 技术特点  支持标准 100M/s 带宽全双工 EtherCAT 总线网络接口及 CoE 通信协议一 进一出&#xff08;RJ45 接口&#xff09;&#xff0c;支持多组动态 PDO 分组和对象字典的自动映射&#xff0c;支持站 号 ID 的自动设置与保存&#xff0c;支持 SDO 的电机参数设置与…...

Upload-labs十六和十七关

目录 第十六关第十七关 第十六关 直接上传php文件判断限制方式&#xff1a; 同第十五关白名单限制 第十六关源码&#xff1a; 代码逻辑判断了后缀名、content-type&#xff0c;以及利用imagecreatefromgif判断是否为gif图片&#xff0c;最后再做了一次二次渲染 第71行检测…...

软件包的管理

概念 在早期Linux系统中&#xff0c;要想在Linux系统中安装软件只能采取编译源码包的方式进行安装&#xff0c;所以早期安装软件是一件非常困难、耗费耐心的事情&#xff0c;而且大多数服务程序仅提供源代码&#xff0c;还需要运维人员编译后自行解决软件之间的依赖关系。所以…...

常见入门级进销存系统合集

进销存系统是企业管理中不可或缺的一环&#xff0c;它们可以帮助企业有效管理库存、销售和采购等关键业务。然而&#xff0c;对于初创企业和小型企业来说&#xff0c;选择一个合适的进销存系统可能是一项挑战。在这篇文章中&#xff0c;我们将探讨入门级和资深级进销存系统之间…...

爬虫逆向实战(32)-某号店登录(RSA、补环境、混淆)

一、数据接口分析 主页地址&#xff1a;某号店 1、抓包 通过抓包可以发现登录接口是/publicPassport/login.do 2、判断是否有加密参数 请求参数是否加密&#xff1f; 通过查看“载荷”模块可以发现&#xff0c;有三个加密参数&#xff1a;username、password、captchaTok…...

正则表达式学习和高级用法

以下所有的验证都在 在线验证 1. 起始符 / 正则表达式的起始符2. 限定符 匹配前面的子表达式**1次或多次**。例如&#xff0c;zo 能匹配 "zo" 以及"zoo"&#xff0c;但不能匹配 "z"。等价于 {1,}。 ? 匹配前面的子表达式**0次或1次**。例如…...

C# Onnx Yolov8 Fire Detect 火焰识别,火灾检测

效果 项目 代码 using Microsoft.ML.OnnxRuntime.Tensors; using Microsoft.ML.OnnxRuntime; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using Syste…...

线程安全问题

目录 一、线程安全 二、线程安全问题 三、线程安全 1.同步代码块 2.同步方法 3.Lock锁 3.1常用方法&#xff1a; 3.2 死锁 3.3 练习&#xff1a; 四、生产者和消费者&#xff08;线程通信问题&#xff09; 一、线程安全 如果有多个线程在同时运行&#xff0c;而这些…...

【力扣每日一题】2023.9.18 打家劫舍Ⅲ

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 今天是打家劫舍3&#xff0c;明天估计就是打家劫舍4了。 今天的打家劫舍不太一样&#xff0c;改成二叉树了&#xff0c;不过规则没有变&…...

Docker基础学习

Docker 学习目标&#xff1a; 掌握Docker基础知识&#xff0c;能够理解Docker镜像与容器的概念 完成Docker安装与启动 掌握Docker镜像与容器相关命令 掌握Tomcat Nginx 等软件的常用应用的安装 掌握docker迁移与备份相关命令 能够运用Dockerfile编写创建容器的脚本 能够…...

esbuild中文文档-路径解析配置项(Path resolution - Alias、Conditions)

文章目录 路径解析配置项 Path resolution别名 Alias条件解析 Conditionsconditions是如何工作的 结语 哈喽&#xff0c;大家好&#xff01;我是「励志前端小黑哥」&#xff0c;我带着最新发布的文章又来了&#xff01; 老规矩&#xff0c;小手动起来~点赞关注不迷路&#xff0…...

您的应用存在隐藏最近任务列表名称的行为,不符合华为应用市场审核标准

最近各家应用市场&#xff0c;唯独华为审核被拒了。。理由是您的应用存在隐藏最近任务列表名称的行为&#xff0c;不符合华为应用市场审核标准。 根据华为给出的视频&#xff0c;app在任务队列&#xff08;也就是俗称的安卓多任务管理后台&#xff09;不显示应用名。因为我们ap…...

Spring的 webFlux 和 webMVC

看到一个测评文章&#xff0c;并发在300的时候webMVC 和 webFlux的处理能力不相上下&#xff0c; 当并发达到3000的时候, webFlux明显优于webMVC, 有图有真相&#xff0c; 我信了. webMVC 是 one-request-one thread 堵塞模式, flux是非阻塞模式&#xff0c; 是spring家族系列…...

【洛谷算法题】P5706-再分肥宅水【入门1顺序结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5706-再分肥宅水【入门1顺序结构】&#x1f30f;题目描述&#x1f30f;输入格式…...

寿光专业做网站的公司/域名注册服务网站哪个好

由于 WordPress大部分的主题都是使用Google的在线字体方案&#xff1a;Google Fonts. Google服务不稳定&#xff0c;导致大量独立博客字体加载不出来&#xff0c;直接导致博客打开速度变慢&#xff0c;严重时甚至导致网站打不开. 解决方案1&#xff1a; 编辑 WordPress 中的文…...

商用网站开发计划书/沈阳网络优化培训

在ASP.NET中&#xff0c;有很多种保存信息的对象。例如&#xff1a;Application&#xff0c;Session&#xff0c;Cookie&#xff0c;ViewState和Cache等&#xff0c;那么它们有什么区别呢&#xff1f;每一种对象应用的环境是什么&#xff1f; 为了更清楚的了解&#xff0c;我们…...

个人网站免费模板/怎么搭建属于自己的网站

2019独角兽企业重金招聘Python工程师标准>>> 一、在编码过程发现报错&#xff1a;Override is not allowed when implementing interface method 。 1、在project/module上&#xff0c;右键选择菜单“Open Module Settings” ; 2、选择Modules&#xff0c;修改“Lan…...

自己做的网站怎么维护/网站性能优化的方法有哪些

文章目录1. 前提条件2. 功能3. 概念4. Helm组件5. 安装5.1 安装客户端5.2 安装服务端6. helm init6.1 --history-max6.2 --node-selectors6.3 --override6.4 --output6.5 其他参数7. 部署示例参考资料&#xff1a; 阳明官方文档&#xff08;含有更多细节&#xff09;kubernete…...

银川做网站哪家好/江苏建站

给大家推荐一个实用面试题库 1、前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 前端面试基础知识题 1. js中如何判断一个值是否是数组类型&#xff1f; instanceof const arr []; arr instanceof…...

做淘宝客如何引出图片到网站/百度云资源搜索引擎

View Code deletefromStationInfo whereStationID in(selectStationID fromStationInfo groupbyStationID havingCOUNT(StationID)>1)转载于:https://www.cnblogs.com/kingteach/archive/2011/05/23/2054259.html...