分类 Linux 下的文章

以下内容来自DeepSeek

证书最初可能是通过 certbot certonly --manual(手动交互模式)申请的,这种方式需要人工操作(如手动添加DNS记录或HTTP验证文件)。
自动续订时,Certbot 会尝试使用相同的插件(manual),但非交互模式下必须通过 --manual-auth-hook 提供自动化验证脚本,否则会失败。

解决方案
如果不需要手动验证(如DNS验证),改用 webroot 或 standalone 模式重新申请证书:

删除旧证书配置:
bash
sudo certbot delete --cert-name <sitedomain>
重新申请证书(以 webroot 模式为例) :
bash
sudo certbot certonly --webroot -w /var/www/html -d <sitedomain>
-w /var/www/html: 指定网站根目录(根据你的实际路径修改)。
-d <sitedomain>: 你的域名。
后续自动续订:
bash
sudo certbot renew --quiet
webroot 模式无需停止Web服务,适合自动化续订。

☁  ~  sudo certbot delete --cert-name <sitedomain>
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The following certificate(s) are selected for deletion:

  * <sitedomain>

WARNING: Before continuing, ensure that the listed certificates are not being
used by any installed server software (e.g. Apache, nginx, mail servers).
Deleting a certificate that is still being used will cause the server software
to stop working. See https://certbot.org/deleting-certs for information on
deleting certificates safely.

Are you sure you want to delete the above certificate(s)?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: yes
Deleted all files relating to certificate <sitedomain>.
☁  ~  sudo certbot certonly --webroot -w /var/www/html -d <sitedomain>
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for <sitedomain>

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/<sitedomain>/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/<sitedomain>/privkey.pem
This certificate expires on 2025-07-02.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
☁  ~  sudo certbot renew --quiet

手册 https://qref.sourceforge.net/Debian/reference/ch-preface.zh-cn.html
WIKI https://wiki.debian.org/zh_CN/FrontPage


昨天大扫除,老黄给了我一只英雄牌的钢笔
又想练字了

2024.12.23本站成功复活

1.update apt
2.install zsh oh-my-zsh manpages-zh
3.config zsh-plugin
4.install apache php
5.config /etc/apache2/site-available #配置域名映射,以及ssl证书
5.test apache testing page
6.install typecho
7.config typecho
8.restore backup


zsh不会默认执行~/.profile,只有sh/bash会执行该配置。

IMG_4570.png

原文:https://www.cnblogs.com/wspblog/p/4710617.html

rc.d的内容如下:
init.d/ :各种服务器和程序的二进制文件存放目录。
rcx.d/: 各个启动级别的执行程序连接目录。里头的东西都是指向init.d/的一些软连接。具体的后边叙述。
还有三个脚本:rc.sysinit, rc, rc.local

redhat的启动方式和执行次序是:
加载内核
执行init程序
/etc/rc.d/rc.sysinit # 由init执行的第一个脚本
/etc/rc.d/rc $RUNLEVEL # $RUNLEVEL为缺省的运行模式
/etc/rc.d/rc.local
/sbin/mingetty # 等待用户登录

在Redhat中,/etc/rc.d/rc.sysinit主要做在各个运行模式中相同的初始化工作,包括:
调入keymap以及系统字体
启动swapping
设置主机名
设置NIS域名
检查(fsck)并mount文件系统
打开quota
装载声卡模块
设置系统时钟
等等。

/etc/rc.d/rc则根据其参数指定的运行模式(运行级别,你在inittab文件中可以设置)来执行相应目录下的脚本。凡是以Kxx开头的
,都以stop为参数来调用;凡是以Sxx开头的,都以start为参数来调用。调用的顺序按xx
从小到大来执行。例如,假设缺省的运行模式是3,/etc/rc.d/rc就会按上述方式调用
/etc/rc.d/rc3.d/下的脚本。
值得一提的是,Redhat中的运行模式2、3、5都把/etc/rc.d/rc.local做为初始化脚本中
的最后一个,所以用户可以自己在这个文件中添加一些需要在其他初始化工作之后,登录之前执行的命令。

init在等待/etc/rc.d/rc执行完毕之后(因为在/etc/inittab中/etc/rc.d/rc的
action是wait),将在指定的各个虚拟终端上运行/sbin/mingetty,等待用户的登录。
至此,LINUX的启动结束。

本文如无特殊解释,init.d指的就是/etc/rc.d/init.d目录。

本文包括3部分内容
1、 Linux的引导过程
2、 运行级别
3、 /etc/rc.d/ 与/etc/rc.d/init.d的关系

/etc/rc.d/init.d/目录下的脚本就类似与windows中的注册表,在系统启动的时候某些指定脚本将被执行”。开始之前,先引用李善明经理昨天晚上总结时的一个理解,让大家先对init.d目录有个大概的印象。在进入init.d之前,我们一起来做两个准备工作,linux的引导过程和运行级别的概念。

一、Linux的引导过程
系统加电之后,首先进行的硬件自检,然后是bootloader对系统的初始化,加载内核。

内核被加载到内存中之后,就开始执行了。一旦内核启动运行,对硬件的检测就会决定需要对哪些设备驱动程序进行初始化。从这里开始,内核就能够挂装根文件系统(这个过程类似于Windows识别并存取C盘的过程)。内核挂装了根文件系统,并已初始化所有的设备驱动程序和数据结构等之后,就通过启动一个叫init的用户级程序,完成引导进程。

二、运行级别(run level)
Init进程是系统启动之后的第一个用户进程,所以它的pid(进程编号)始终为1。 init进程上来首先做的事是去读取/etc/目录下inittab文件中initdefault id值 ,这个值称为运行级别(run-level)。它决定了系统启动之后运行于什么级别。运行级别决定了系统启动的绝大部分行为和目的。这个级别从0到6 ,具有不同的功能。不同的运行级定义如下:

# 0 - 停机(千万别把initdefault设置为0,否则系统永远无法启动)
# 1 - 单用户模式
# 2 - 多用户,没有 NFS
# 3 - 完全多用户模式(标准的运行级)
# 4 – 系统保留的
# 5 - X11 (x window)
# 6 - 重新启动 (千万不要把initdefault 设置为6,否则将一直在重启 )

三、/etc/rc.d/与/etc/rc.d/init.d的关系
写到这里,应该差不多要进入init.d了,可是我觉得单写/etc/rc.d/init.d的话不一定能说得清楚明白,就拿它跟/etc/rc.d这个它上一级的目录一起来讨论,可能比较合适一些,因为他们之间有着千丝万缕的关系。

在这里先解释一下init.d里面放的都是什么东西。这个目录存放的是一些脚本,一般是linux以rpm包安装时设定的一些服务的启动脚本。系统在安装时装了好多rpm包,这里面就有很多对应的脚本。执行这些脚本可以用来启动,停止,重启这些服务。

前面说到,/etc/rc.d/init.d这个目录下的脚本就类似与windows中的注册表,在系统启动的时候执行。程序运行到这里(init进程读取了运行级别),相信从命名的角度大家也能猜到该运行/etc/rc.d/init.d里面的脚本了,不然它为什么也叫init(.d)呢是吧。没错,是该运行init.d里的脚本了,但是并不是直接运行,而是有选择的因为系统并不需要启动所有的服务。

那么,系统是如何选择哪些需要启动哪些不要呢?这时刚才说的运行级别就起作用了。

在决定了系统启动的run level之后,/etc/rc.d/rc这个脚本先执行。在RH9和FC7的源码中它都是一上来就check_runlevel()(虽然实现的代码不一样,也大同小异),知道了运行级别之后,对于每一个运行级别,在rc.d下都有一个子目录分别是rc0.d,rc1.d ….. rc6.d。每个目录下都是到init.d目录的一部分脚本一些链接。每个级别要执行哪些服务就在相对应的目录下,比如级别5要启动的服务就都放在rc5.d下,但是放在这个rc5.d下的都是一些链接文件,链接到init.d中相对应的文件,真正干活的init.d里的脚本。

到这里,估计大家可能都比较清楚了,我开始也以为是这样的。可是后来我仔细看过和比较这些链接文件和init.d里真正被执行的脚本的文件名之后,一直有几个问题没弄明白。借着写这个文章的机会,我做了一些功课,总算是大概解开了那些疑惑。

1、这些链接文件前面为什么会带一个Kxx或者Sxx呢?
是这样的,带K的表示停止(Kill)一个服务,S表示开启(Start)的意思

2、K和S后面带的数字呢?干什么用的
这个我开始的时候还以为是排列起来好看或者数数用呢(是不是很幼稚?)。后来发现不是的。它的作用是用来排序,就是决定这些脚本执行的顺序,数值小的先执行,数值大的后执行。很多时候这些执行顺序是很重要的,比如要启动Apache服务,就必须先配置网络接口,不然一个没有IP的机子来启动http服务那岂不是很搞笑。

3、无意中我发现同一个服务带S的和带K的链接到init.d之后是同一个脚本。我就纳闷了,为什么会是执行同一个脚本呢?
这个时候真是S和K的妙用了,原来S和K并不止是用来看起来分的清楚而已。S给和K还分别给init.d下面的脚本传递了start和stop的参数。哦,是这样的(焕然大悟的样子,呵呵)!这时我才想起来原来曾经无数用过的/etc/rc.d/init.d/network restart命令。原来传S时相当于执行了/etc/rc.d/init.d/xxx start这条命令,当然K就相当于/etc/rc.d/init.d/xxx stop了。

选项

-i 直接写入
-e 拼接指令
-n 只打印经过编辑的行
-u 禁用输出缓冲,即实时输出结果

替换标记

/g global全局替换
/p print将经过替换的行打印输出
## 需要注意的是,如果只使用 /p 标记而不使用 -n 选项,sed 命令将会打印所有经过编辑的行,包括未匹配到的行。
## 而只使用 -n 选项而不使用 /p 标记,则不会打印任何行。因此,两者结合使用时可以过滤并打印出符合条件的行。
/i 忽略大小写替换标记

地址范围选项

n:行号选项,表示只对指定行号的行进行操作。例如,5s/pattern/replacement/ 表示只对第 5 行进行替换操作。
start:起始行选项,表示从指定行开始应用编辑命令。例如,/pattern/s/replacement/ 表示从匹配到 pattern 的行开始进行替换操作。
start,end:起始行和结束行选项,表示在指定的行范围内应用编辑命令。例如,/start/,/end/s/pattern/replacement/ 表示在匹配到 start 行和 end 行之间进行替换操作。
$:最后一行选项,表示对最后一行进行操作。例如,$s/pattern/replacement/ 表示对最后一行进行替换操作。
!:否定行选项,表示对选定行之外的行应用编辑命令。例如,1,3!s/pattern/replacement/ 表示对除了第 1 行到第 3 行之外的所有行进行替换操作。

ssh免密登录

例子:机器A免密登录机器B

# 在A上执行
## 生成密钥对,保存在~/.ssh中,一般有两个文件id_rsa和id_rsa.pub对应私钥和公钥;
## 如果要实现免密登录,在输入passphrase时直接回车,不要输入内容
ssh-keygen -t rsa
scp ~/.ssh/id_rsa.pub username@B_ipAddress:/path/to/temporary_location

# 在B上执行
## 若有多台机器希望免密登录B,在B上的authorized_keys后面追加写入公钥即可
cat /path/to/temporary_location >> ~/.ssh/authorized_keys

passphrase作用是解密私钥


PAM(Pluggable Authentication Modules)可插拔身份认证模块

在/etc/pam.d/文件夹中,每个文件对应一个应用程序或服务。这些文件的命名通常与相应的应用程序或服务的名称相对应。例如sshd文件对应 SSH 服务的身份验证和授权规则,login文件对应本地登录过程的身份验证和授权规则。
提权之后,一种持久化方法是修改PAM。

# 在/etc/pam.d/下文件内容主要依据四个规则:

auth:身份验证规则
    auth required pam_unix.so:使用标准的UNIX身份验证方法进行验证。
    auth required pam_google_authenticator.so nullok:使用Google Authenticator进行两步验证,允许用户在没有启用Google Authenticator的情况下进行身份验证。

account:授权规则
    account required pam_unix.so:使用标准的UNIX账户管理来控制用户的访问权限。
    account required pam_access.so:使用/etc/security/access.conf文件中的规则来控制用户的访问控制。

password:密码管理规则
    password required pam_cracklib.so:使用CrackLib库来检查和强制密码策略。
    password required pam_unix.so sha512 shadow:使用UNIX密码管理和shadow密码文件来存储用户密码。

session:会话管理规则
    session required pam_limits.so:设置用户会话的资源限制,如最大打开文件数和最大进程数。
    session optional pam_systemd.so:与systemd集成,以管理用户会话的生命周期。

举个例子:

# 编辑 /etc/pam.d/login
# 添加或修改以下行
auth required pam_tally2.so deny=5 unlock_time=600
auth required pam_unix.so
account required pam_tally2.so
account required pam_unix.so

此例中,
认证阶段(auth):
pam_tally2.so会首先执行,它会检查登录尝试次数是否超过限制。如果登录尝试次数超过设置的限制(如 deny=5),它将导致认证失败,并且用户账户会被锁定指定时间(如 unlock_time=600)。
即使 pam_tally2.so 失败,pam_unix.so 还是会继续执行。这意味着系统会继续检查用户的用户名和密码是否正确。如果用户名和密码正确,但 pam_tally2.so 失败,整体认证仍然会失败。

账户管理阶段(account):
pam_tally2.so会再次执行,用于更新和检查账户状态,例如解锁被锁定的账户。
然后pam_unix.so会执行,进行标准的账户管理操作,例如检查账户是否已过期。

总结
在PAM配置文件中,每个required模块必须成功,否则整个认证过程将失败。然而,它们会按照配置文件中的顺序依次执行,即使前面的模块失败了,后面的模块也会继续执行,但最终认证结果将取决于所有required模块的结果。


那么是否可以重写pam_unix.so?可以。
只要模块路径正确且模块本身编写和安装正确,PAM 配置文件就能够识别并调用它。
如何编写正确的PAM模块可以参考Linux-PAM GNU
攻击样例可以参考Linux 攻擊場域:SSH 登入攻擊手法初探,文中利用

account sufficient bad_pam.so

指定该模块(bad_pam.so)返回成功(Success)状态,那么就无需再继续执行后面的账户授权规则,即可立即授权通过。而在bad_pam.so中可以操作很多事,比如任意密码登录,登录不被记录,非指定密码(后门账号)登录之后键盘被记录等等。

检测方法

观察/usr/lib/x86_64-linux-gnu/security/和/etc/pam.d/中文件的修改时间hash值是否可疑。

1. fork

CSAPP中对这个比喻很形象,就是从master线中分叉出来了一条子进程,与父进程共享文件描述符和所有其他资源。

#include <unistd.h>

pid_t fork(void);

2. exec

这是一个函数族,包含了几个函数

#include <unistd.h>
 
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
 
#参数说明:
#path:可执行文件的路径名字
#arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
#file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

后缀的意思:
l:表示 "list",这意味着参数以可变参数列表(variable argument list)的形式传递,最后一个参数为 NULL,用于标识参数列表的结束。
p:表示 "path",这意味着在系统的 PATH 环境变量中搜索可执行文件。
e:表示 "environment",这意味着可以指定环境变量。
v:表示 "vector",这意味着参数以数组的形式传递,数组中每个元素都是一个字符串指针。

不管哪种exec,都有一个共性:会用一个程序完全取代当前的程序。
换句话说,这是一个有去无回的动作,可以将当前进程完全转变成另一个进程。

fork和exec是其他的基础。

3. system

参考man-pages,system实际上就是fork和exec的结合。

#include <unistd.h>

pid_t fork(void);

简而言之就是用fork生成一个新进程,然后父进程wait,等待子进程结束。子进程用exec执行命令。

int system(const char * cmdstring)
{
  pid_t pid;
  int status;
 
  if(cmdstring == NULL){
      
      return (1);
  }
  if((pid = fork())<0){
        status = -1;
  }
  else if(pid == 0){
    execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
    -exit(127); //子进程正常执行则不会执行此语句
    }
  else{
        while(waitpid(pid, &status, 0) < 0){
          if(errno != EINTER){
            status = -1;
            break;
          }
        }
    }
    return status;
}

4. spawn

#include <spawn.h>

int posix_spawn(pid_t *restrict pid, const char *restrict path,
                const posix_spawn_file_actions_t *restrict file_actions,
                const posix_spawnattr_t *restrict attrp,
                char *const argv[restrict],
                char *const envp[restrict]);
int posix_spawnp(pid_t *restrict pid, const char *restrict file,
                const posix_spawn_file_actions_t *restrict file_actions,
                const posix_spawnattr_t *restrict attrp,
                char *const argv[restrict],
                char *const envp[restrict]);

按照man-pages的说法,spawn是为了给不支持fork的系统提供标准化方法(standardized method),主要针对小型嵌入式系统。并且文中提到,spawn是syscall实现功能的子集。

两种spawn有什么区别?

With posix_spawnp(), the executable file is specified as a simple filename; the system searches for this file in the list of directories specified by PATH (in the same way as for execvp(3).
也就是说posix_spawnp后缀的p也是指代PATH

那么spawn和system,fork有什么区别呢?
根据多处文档(BlackBerry|qnxOracle Solaris BlogThe Open Group)以及《理解Unix进程》- Jesse Storimer中描述:

system会阻塞到命令完成,而spawn会直接返回。

fork子进程有两个特点:1.获得父进程的文件描述符 2.获得父进程所有内存。而spawn只保留了1,没有保留2。也就是说spawn比fork更轻便,性能更好,在进程切换的过程中开销更小。但也因为没有内存的内容,缺少了灵活性。

从实现层面上,spawn也是通过fork和exec实现,但是和system不同的是在fork之后,子进程中在exec之前做了其他事,而不是像system一样直接调用exec。所以spawn有很多参数,能指定更改环境变量。

5. popen

pipe open
创建一个管道,并启动一个子进程进行命令的执行。它返回一个文件指针,该文件指针指向子进程的标准输入或标准输出,具体取决于 popen 函数的第二个参数。注意,无法一次访问所有的流。
对应还有pclose。

#include <stdio.h>

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

popen 函数返回的文件指针可以像普通文件指针一样使用标准的文件 I/O 函数,如 fread、fwrite、fgets、fputs 等。

inittab

现代Linux已用systemd取代了inittab,并将inittab的功能集成在了systemctl中。可以通过编写适当的.service文件并将其放置在/etc/systemd/system/目录下,然后使用systemctl命令来管理和控制这些服务。这是在现代Linux中定义和管理初始化进程的推荐方式。参考SysVinit

但是现代Linux中依然有部分发行版支持自建/etc/inittab,如Arch

ArchWiki中明确指出:重启之前,务必使用 telinit q 测试修改过的 /etc/inittab,任何小小的语法错误都将导致系统无法启动。

可以通过pstree观察PID为1是否为inittab确定是否可用,以及/etc/inittab是否存在判断。并且,虽然现代Linux用systemd取代了,但是其他Unix系统如AIX,以及老的(占有率其实很高)Linux还在用,所以学这个是有意义的。
每个inittab文件条目由四个字段组成,字段之间使用冒号(:)进行分隔。格式如下:

Identifier:RunLevel:Action:Command

Identifier(标识):一个用于唯一标识该条目的字符串,可以是一个或多个字符。
RunLevel(运行级别):指定可以在哪个运行级别下处理该条目。运行级别与系统中的进程配置相对应,使用数字0到9表示。例如,如果系统处于运行级别1,那么只有RunLevel字段中包含1的条目才会启动。如果没有指定任何运行级别,那么假定该进程在所有运行级别下都有效。
Action(操作):指示初始化命令如何处理指定进程。
    respawn:如果该进程不存在,请启动它。在进程终止时,重新启动该进程。
    wait:启动该进程并等待它的终止。当进入与条目的运行级别匹配的运行级别时,执行该操作。
    once:启动该进程,并且不等待其终止。当它终止时,不要重新启动该进程。
    boot:启动该进程,但不等待其终止。只在系统引导期间执行一次。
    off:在系统引导期间不启动该进程。
Command(命令):要执行的命令或脚本。命令用“”包裹,或者填入脚本路径。

每个条目都以换行符分隔,可以使用反斜杠(\)表示条目的连续。可以在行的开头使用冒号(:)将条目注释掉。
另外要注意Identifier必须唯一,不能冲突,Runlevel不同发行版可能有所不同。

.service

一个典型的.service文件包含以下几个部分:

[Unit]:这个部分定义了服务的基本信息,包括服务名称、描述、依赖关系等。
    Description:描述服务的简短说明。
    Requires:定义服务所依赖的其他服务。
    After:定义服务在哪些其他服务之后启动。
[Service]:这个部分定义了服务的执行参数和行为。
    Type:指定服务的类型,可以是simple(默认值)、forking、oneshot、dbus、notify等。
    ExecStart:指定服务启动时要执行的命令或脚本。
    Restart:定义服务在发生故障时的重启行为。
    WorkingDirectory:指定服务运行的工作目录。
    User和Group:指定服务运行的用户和用户组。
[Install]:这个部分定义了服务的安装配置。
    WantedBy:定义服务在哪个target或目标中激活。

示例

[Unit]
Description=My Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/my-service
Restart=always
WorkingDirectory=/path/to/service
User=myuser
Group=mygroup

[Install]
WantedBy=multi-user.target

Linux

1.__attribute__((constructor)) and __attribute__((destructor))

void __attribute__((constructor)) my_constructor()
{
    printf("加载\n");
}

void __attribute__((destructor)) my_destructor()
{
    printf("卸载\n");
}

constructor会在dll被加载的时候执行,但是要注意一点:

void* dlopen(const char* filename, int flags);
//flags: RTLD_NOW, RTLD_LAZY

RTLD_NOW的时候constructor立即被执行,RTLD_LAZY当so中函数被调用的时候才会执行constructor。

2.执行了两次destructor

__attribute__((destructor))在使用 dlclose() 显式关闭共享库句柄时会在共享库被卸载之前调用,在程序结束时,无论是否显式调用了 dlclose(),共享库句柄会被自动关闭,__attribute__((destructor)) 声明的函数也会在共享库被卸载之前被调用。
这就意味着destructor可能被执行两次

void* handle = dlopen("./libexample.so", RTLD_LAZY);
dlclose(handle);

此时若只希望执行一次,可以考虑:

bool is_destructor_called = false;

void __attribute__((destructor)) my_destructor()
{
    if (!is_destructor_called) {
        // 执行析构操作
        is_destructor_called = true;
    }
}

[WARNING]不要显式调用__attribute__((constructor))/__attribute__((destructor))!


Windows

1.BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);

DllMain是DLL的入口点函数,当DLL被加载、卸载、线程附加或线程分离时,系统会自动调用DllMain函数。

hModule:表示当前被加载的DLL模块的句柄。

ul_reason_for_call:标识了调用DllMain的原因,它可能取以下值:
    DLL_PROCESS_ATTACH:表示DLL被加载到进程时调用。
    DLL_PROCESS_DETACH:表示DLL从进程中卸载时调用。
    DLL_THREAD_ATTACH:表示DLL被线程附加时调用。
    DLL_THREAD_DETACH:表示DLL从线程中分离时调用。

lpReserved:保留参数,一般情况下不使用,应设置为NULL。

一般进入DllMain可以首先switch:

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){
    switch (ul_reason_for_call){
        case DLL_PROCESS_ATTACH:
            // 此处处理动态链接库被进程加载的事件
            // 可以在这里进行一些初始化操作
            break;

        case DLL_PROCESS_DETACH:
            // 此处处理动态链接库被进程卸载的事件
            // 可以在这里进行一些清理操作
            break;

        case DLL_THREAD_ATTACH:
            // 此处处理动态链接库被线程加载的事件
            break;

        case DLL_THREAD_DETACH:
            // 此处处理动态链接库被线程卸载的事件
            break;
    }
    return TRUE;
}

2.__declspec(dllexport)

根据编译器的不同,没有经过__declspec(dllexport)声明的函数可能不会导出到dll的导出表。
此点可为debug用。

3.白加黑不要轻易修改DllMain()

如果修改,可能导致死锁。

// 声明函数指针类型
typedef void (*FuncPtr)();

int main()
{
    // 加载 DLL
    HINSTANCE hDll = LoadLibrary("mydll.dll");
    if (hDll == NULL)
    {
        // 处理加载 DLL 失败的情况
        return 1;
    }
    // 获取函数指针
    FuncPtr pFunc2 = (FuncPtr)GetProcAddress(hDll, "func2");
    if (pFunc2 == NULL)
    {
        // 处理获取函数指针失败的情况
        // 释放 DLL
        FreeLibrary(hDll);
        return 1;
    }
    // 调用 func2 函数
    pFunc2();
    // 释放 DLL
    FreeLibrary(hDll);

    return 0;
}

当一个线程调用 LoadLibrary 加载被注入DLL时,操作系统会自动调用被注入DLL中的 DllMain 函数,并传递 DLL_PROCESS_ATTACH 参数。在 DllMain 函数中,可能会进行一些初始化操作或创建线程等。此时,如果同时有另一个线程尝试修改或替换被注入DLL,也会触发 LoadLibrary 调用,并导致另一个 DllMain 函数的调用。
由于 DllMain 函数没有进行适当的同步处理,多个线程同时调用 DllMain 函数时,可能会发生死锁。