记一次linux操作系统实验

前言

最近完成了一个需要修改和编译linux内核源码的操作系统实验,个人感觉这个实验还是比较有意思的。这次实验总共耗时4天,从对linux实现零基础,通过查阅资料和不断尝试,直到完成实验目标,在这过程中确实也收获颇丰,特此记录

实验内容

  1. 实现系统调用int hide(pid_t pid, int on),在进程pid有效的前提下,如果on置1,进程被隐藏,用户无法通过ps或top观察到进程状态;如果on置0且此前为隐藏状态,则恢复正常状态(考虑权限问题,只有root用户才能隐藏进程)
  2. 设计一个新的系统调用int hide_user_processes(uid_t uid, char *binname),参数uid为用户ID号,当binname参数为NULL时,隐藏该用户的所有进程;否则,隐藏二进制映像名为binname的用户进程
  3. 在/proc目录下创建一个文件/proc/hidden,该文件可读可写,对应一个全局变量hidden_flag,当hidden_flag为0时,所有进程都无法隐藏,即便此前进程被hide系统调用要求隐藏。只有当hidden_flag为1时,此前通过hide调用要求被屏蔽的进程才隐藏起来
  4. 在/proc目录下创建一个文件/proc/hidden_process,该文件的内容包含所有被隐藏进程的pid,各pid之间用空格分开

实现思路

对于要求1,首先要修改PCB,对应到源码里面就是task_struct,在其中添加一个属性hide,用来表示该进程是否需要隐藏;然后修改复制进程的系统调用,用于给hide属性设置默认值0;最后修改列举所有进程的系统调用,在其中加入一个判断,如果进程的hide是1则不展示这个进程
(注:也有方法说是可以通过把pid设置为0来达到隐藏的效果,但是实测下来,在5.15.60的kernel里面,这样做不能隐藏,所以只能通过劫持系统调用来实现)

对于要求2,则可以遍历所有进程,把符合条件的进程的hide设置为1即可

对于要求3,最开始以为可以通过用户态的文件操作来实现,结果后来发现/proc是个虚拟文件系统,所以需要在初始化proc文件系统时,添加一个hide条目,然后设置这个条目的write函数,来达到创建该文件的目的

对于要求4,也是用和要求3一样的思路,只是这里需要设置read函数,然后遍历所有进程,把hide为1的pid全部返回

实验环境

操作系统使用的ubuntu 22.04
linux kernel代码版本是5.15.60
虚拟机使用的是VM Ware Workstation Pro 16
注意:虚拟机硬盘大小建议为60GB,编译内核代码非常吃硬盘,本人在实验中前前后后扩容了几次硬盘,最终发现60GB是个比较合适的大小,内核源码编译安装之后还能剩15GB左右(下一次编译安装还需要一些硬盘空间做缓存,所以剩15GB是比较合适的)

实验流程

编译与安装内核

参考https://www.cnblogs.com/robotech/p/16152269.html 即可,如果这部分出错了,网上可以找到的资料很多,这里不再赘述了
不过这一步一定要有耐心,源码编译很慢,第一次全量编译估计会耗时一个多小时,可以用这个闲暇时间玩玩原神

完成要求一

把编译和安装的流程跑通以后,就开始进行源代码的修改了

修改PCB

linux的PCB结构体是task_struct,这个定义位于include/linux/sched.h
Tips:如果想在linux源码里找东西,可以用https://elixir.bootlin.com/ 这个网站,左边选择版本,右边输入关键字,即可查询到
在这里插入图片描述
在linux使用vim打开这个文件,往下翻,看到这段注释
在这里插入图片描述
按照提示添加属性即可

修改fork

这部分的源码是在kernel/fork.c中的copy_process函数里面
阅读源码,在合适的地方插入初始化hide属性的代码即可,这里我选择的位置是复制完进程信息之后,即下图所示的位置
在这里插入图片描述

添加系统调用

这部分我是看网上的各种文章,东拼西凑,进行多次实验之后才跑通的,事后想想,我应该最先去看linux kernel的官方手册
https://docs.kernel.org/process/adding-syscalls.html (附上官方手册)
以下是我自己添加系统调用的过程,这里用添加一个输出Hello World的简单系统调用来举例子
首先找到kernel/sys.c,在文件末尾使用SYSCALL_DEFINE宏来定义系统调用的函数体

SYSCALL_DEFINE0(hello)
{printk("hello world.114514\n");return 0;
}

解释一下,SYSCALL_DEFINE0(hello)表示定义一个含有0个参数的系统调用,名字是hello,通过查看sys.c里面其它函数的定义代码可以得知,如果想要添加一个只有一个参数的系统调用,那么应该使用SYSCALL_DEFINE1(hide,pid_t,pid),其中hide是系统调用的名字,pid_t是第一个参数类型,pid是第一个参数的名字,2个参数的同理
printk是输出日志,这个日志可以在sudo dmesg里面看到,printk支持使用%d,%s等对输出进行格式化,用法类似于printf

接下来修改系统调用表,在arch/x86/entry/syscalls/syscall_64.tbl中合适的地方添加刚才写的系统调用,这里我是添加在了334号系统调用之后的
在这里插入图片描述
仿照上面334写即可,其中hello是自己随便起的名字,而sys_hello是系统调用的函数名,这个函数名是上面SYSCALL_DEFINE0里写的函数名前面加上前缀sys_得到的

添加完成后可以尝试编译运行一下新的内核
可以使用uname -a查看当前内核是不是最新编译的(看时间即可)
在这里插入图片描述
下面将使用一段代码来测试一下新添加的335号系统调用

#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>int main(int argc,char **argv)
{printf("System call return %ld\n",syscall(335));return 0;
}

运行程序
在这里插入图片描述
然后使用sudo dmesg查看
在这里插入图片描述

编写hide系统调用

hide系统调用的实现有2个思路,一个是遍历所有进程,找到pid相符的进程,然后设置hide,另一个思路是通过pid找到进程,然后直接设置,这里采取后者
通过查找资料,可以得知,根据pid查找进程是用这段代码
pid_task(find_vpid(pid),PIDTYPE_PID);
最终完整的系统调用代码如下

SYSCALL_DEFINE2(hide,pid_t,pid,int,on)
{struct task_struct * me = NULL;me=pid_task(find_vpid(pid),PIDTYPE_PID);if(current->uid != 0){//User is not rootreturn 0;}if(me == NULL){return 0;}if( on == 1 ){me->hide = 1;}else{if( me->hide == 1 ){me->hide = 0;}}return 0;
}

接下来再修改系统调用表即可完成系统调用的添加

劫持获取所有进程的函数

现在已经可以通过系统调用来设置PCB里面的hide,下一步就是修改列举所有进程的函数,让它在列举时判断一下,如果hide==1就不列举

proc文件系统

在劫持之前,需要简单介绍一下proc文件系统。在linux根目录下,有一个/proc文件夹,这其实并不是在磁盘上真实存在的文件,而是一个虚拟文件系统。
proc文件夹里面有很多个以pid为名字的文件夹,这些文件夹里面又有若干个文件,读取这些文件就可以获取这个进程的相关信息,例如想查看pid为1的程序的名字可以使用sudo cat /proc/1/comm
这一系列操作在系统底层的实现是:系统在启动的时候就挂载了一个proc虚拟文件系统,当用户访问proc文件夹下的文件时,系统会调用proc文件系统里面相关的函数,而不是常规文件系统的函数,例如在执行ls /proc时,实际上系统会调用位于s/proc/base.c里面的proc_pid_readdir函数,这个函数会获取当前系统中所有的进程,随后会有函数把这个函数的返回值写入到读取文件操作的缓冲区中

修改代码

所以,我们的突破口就是proc_pid_readdir函数,在阅读这个函数的代码之后,可以找到突破口是一个put_task_struct函数的调用,如下图
在这里插入图片描述
那么只需要在这个if里面加上一个条件,即必须这个进程不被隐藏才能put,即可完成劫持

结果验证

在修改完源代码之后,重新编译和安装内核,启动新的内核
使用下面这段代码来测试

#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>int main(int argc,char **argv)
{int pid;int hide;scanf("%d %d",&pid,&hide);printf("System call return %ld\n",syscall(336,pid,hide));return 0;
}

编译运行程序
在这里插入图片描述
从图中可以看出,进程顺利隐藏,并且能够重新展示,要求一顺利实现

完成要求二

要求二是在要求一的基础上进行一些简单的扩展,这里可以使用一个比较暴力的思路,就是遍历所有进程,然后挨个判断uid和进程名称,把符合要求的进程的hide设置为1即可
这里只有三点需要注意一下
1.遍历所有进程可以使用for_each_process这个宏来完成,这个宏有类似于for循环的作用,用法如下

struct task_struct* p;
for_each_process(p){//Do something.....
}

这个宏的定义在include/linux/sched/signal.h里面,定义如下

#define for_each_process(p) \for (p = &init_task ; (p = next_task(p)) != &init_task ; )

2.用户态的字符串不能在内核态直接使用,需要调用strncpy_from_user把用户态的字符串复制到内核态的缓冲区才能使用,方法如下

char tmp_buf[256];
if(binname != NULL)strncpy_from_user(tmp_buf,binname,256);

最终的系统调用代码如下

SYSCALL_DEFINE2(hide_user_processes,uid_t,uid,char*,binname)
{uid_t curr_uid=current->uid;if(curr_uid != 0){//User is not rootreturn 0;}char tmp_buf[256];if(binname != NULL)strncpy_from_user(tmp_buf,binname,256);struct task_struct* p=NULL;for_each_process(p){if(p->real_cred->uid.val == uid){if(binname == NULL){p->hide=1;}else{char* s=p->comm;int identical=1;int i=0;for(i=0;tmp_buf[i]!='\0' && s[i] != '\0';i++){if(tmp_buf[i] != s[i]){identical=0;break;}}if(tmp_buf[i] != s[i])identical=0;if(identical == 1){p->hide=1;}}}}return 0;
}

在编译和安装完成之后可以写一段测试代码来验证一下代码的正确性

#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>int main(int argc,char **argv)
{int uid;char binname[20];scanf("%d %s",&uid,binname);printf("%s\n",binname);bool noBinname=false;if(strcmp(binname,"no") == 0){printf("Bin name set null\n");noBinname=true;}printf("System call return %ld\n",syscall(337,uid,noBinname?NULL:binname));return 0;
}

编译运行该程序
在这里插入图片描述
要求二完成

完成要求三

思路分析

之前提到过的,proc文件系统是一个虚拟文件系统,读取和写入proc文件夹下的文件的操作会交给一些特定的内核函数来执行,那么我们只需要添加一个/proc/hide条目,并配置这个条目的write函数,当write被调用的时候就根据写入的值设置一个全局变量,然后再修改proc_pid_readdir函数,添加一个判断,如果这个全局变量为0就不隐藏任何进程,这样就可以达到设置全局开关的目的

全局变量的定义和使用

全局变量可以跨文件被使用,在需要使用全局变量的地方使用extern关键字声明全局变量即可
需要注意的是,全局变量需要进行一次初始化,并且仅可以进行一次初始化
具体而言,可以这样操作:在需要使用全局变量hidden_flag的c文件里面使用下面这条语句进行声明

extern int hidden_flag;

然后在某个c文件中对hidden_flag变量进行定义

extern int hidden_flag;
int hidden_flag=1;

注意:声明是告诉编译器我这里有一个名叫hidden_flag的变量,我接下来会用这个变量,这个变量具体在哪需要编译器自己去找;而定义则是告诉编译器我新建了一个名为hidden_flag的变量,相当于真正为这个变量分配了内存空间

添加proc条目

大体流程

通过查阅资料和反复实验,我找到了在5.15.60版本添加proc条目的方法
proc文件系统的初始化函数在fs/proc/root.c里面,名叫proc_root_init
网上很多教材是要修改一个名叫proc_misc_init函数,但是在这个版本的内核源码里面找不到这个函数,所以索性就在proc_root_init函数里面添加条目了(因为看网上的代码,root_init是会调用misc_init的,所以猜测直接在root_init里面添加应该也是可以的,最后实践证明确实可行)

添加proc条目需要调用proc_create函数,该函数的定义如下:

struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct proc_ops *proc_ops);

可以看到,这个函数需要4个参数,第一个是文件名,这里要创建一个/proc/hidden,所以这个参数传hidden;第二个参数是权限,为了防止后续因为权限问题导致实验翻车,这里就给666了;第三个是parent,传NULL即可;第四个是这个条目操作的配置项的指针,可以在这里配置该条目的read和write函数

下面开始添加proc条目
首先要实现该条目的read和write函数,当用户态程序读取和写入/proc/hidden时,这两个函数就会被调用

read函数

下面是read函数的定义

ssize_t hidden_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp);

第一个参数是文件;第二个参数是用户态的读取缓冲区,我们需要往这个缓冲区里面写数据来完成读取操作;第三个参数是这个用户态缓冲区的大小;第四个参数是上一次读取的位置,因为可能出现缓冲区不够等情况,用户态程序在读文件时通常是用下面的方式进行多次读取的

char buf[256];
int len;
while((len=read(buf))!=0){//此时buf中读取了len字节的数据,进行相应处理
}

所以read函数要做的事情就是往缓冲区中写入数据,修改offp,然后返回已经读入的字节数,下面是/proc/hidden条目的read函数的实现

ssize_t hidden_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp )
{if(*offp > 0)return 0;char msg[256];int len=sprintf(msg,"Current flag is %d\n",hidden_flag);copy_to_user(buf,msg,len);*offp=len;return len;
}

需要注意的是,如果,没有最开始判断offp这一行,那么会出现读取/proc/hidden文件读不完的情况,具体而言,如果使用指令cat /proc/hidden,那么它会一直源源不断地蹦出字符,不会停,这是因为read函数始终不会返回0,导致那个while循环不会停
此外,同样的,内核态的内存和用户态的内存是不互通的,需要使用copy_to_user函数来完成内存的拷贝

write函数

write函数的定义如下

ssize_t hidden_write_proc(struct file *filp,const char *buf,size_t count,loff_t *offp);

参数的意义和read是类似的,第二个参数是用户即将写入的数据缓冲区地址,第三个则是数据量,以下是write函数的具体实现

ssize_t hidden_write_proc(struct file *filp,const char *buf,size_t count,loff_t *offp)
{char msg[2056];copy_from_user(msg,buf,count);hidden_flag=msg[0]-'0';return count;
}

需要注意的是,同样的,需要进行从用户态到内核态的内存拷贝

proc_ops结构体

接下来新建一个proc_ops结构体的对象,传入我们写的read和write函数

struct proc_ops hidden_proc_fops = {proc_read:  hidden_read_proc,proc_write: hidden_write_proc
};

当然,这个结构体还支持我们配置更多的内容,具体可以看这个结构体的定义,这里不再赘述了

调用proc_create

最后调用proc_create,传入参数,即可完成条目的创建

proc_create("hidden",666,NULL,&hidden_proc_fops);

结果验证

我们重新编译安装内核,然后隐藏一个进程,然后再向/proc/hidden里面写入0
在这里插入图片描述
可见,hidden_flag起效果了,要求三完成

完成要求四

有了要求三的铺垫,要求四就显得比较简单了,只需要实现一个read函数,在其中遍历所有进程,把hide为1的进程pid返回即可
read函数的实现如下

ssize_t pid_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp ) 
{if(*offp > 0)return 0;char msg[1024];int len=0;struct task_struct* p;for_each_process(p){if(p->hide == 1){len += sprintf(msg+len,"%d ",p->pid);}}copy_to_user(buf,msg,len);*offp=len;return len;
}

可以把1000用户所有进程隐藏了,然后查看/proc/hidden_process文件来检查效果
在这里插入图片描述
可以在里面看到所有被隐藏的进程的pid,要求四完成

总结与心得

这次实验的代码量并不多,操作步骤也不复杂,主要的时间都花在了学习linux内核编程上面了。从零开始学习proc文件系统,linux源码,并建立临时知识体系,然后根据学到的东西进行开发实践,这是一个充满挑战性但也非常有意思的过程。在这过程中,我学到了linux内核编程的技术,跑通了从内核源码修改到最终运行的全流程,并对proc虚拟文件系统进行了更深入的自学,完成了四个实验要求。
个人感觉这过程中查资料自学的效率有点低,下次遇到此类问题应该首先查找官方的手册和教程,而不是在网上胡乱找相关的文章。
总的而言,收获很多,这是一次非常有意思的经历。
Anyway,写这篇博客也是记录一下这次实验的经历,感悟和收获,同时也为其他做这个实验的同学提供一点过来人的经验,希望能起到避坑的效果。

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

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

相关文章

【黑马甄选离线数仓day04_维度域开发】

1. 维度主题表数据导出 1.1 PostgreSQL介绍 PostgreSQL 是一个功能强大的开源对象关系数据库系统&#xff0c;它使用和扩展了 SQL 语言&#xff0c;并结合了许多安全存储和扩展最复杂数据工作负载的功能。 官方网址&#xff1a;PostgreSQL: The worlds most advanced open s…

Springboot将多个图片导出成zip压缩包

Springboot将多个图片导出成zip压缩包 将多个图片导出成zip压缩包 /*** 判断时间差是否超过6小时* param startTime 开始时间* param endTime 结束时间* return*/public static boolean isWithin6Hours(String startTime, String endTime) {// 定义日期时间格式DateTimeFormatt…

【数据结构】—搜索二叉树(C++实现,超详细!)

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;消えてしまいそうです—真夜中 1:15━━━━━━️&#x1f49f;──────── 4:18 &#x1f504; ◀️ ⏸ ▶️…

函数计算的新征程:使用 Laf 构建 AI 知识库

Laf 已成功上架 Sealos 模板市场&#xff0c;可通过 Laf 应用模板来一键部署&#xff01; 这意味着 Laf 在私有化部署上的扩展性得到了极大的提升。 Sealos 作为一个功能强大的云操作系统&#xff0c;能够秒级创建多种高可用数据库&#xff0c;如 MySQL、PostgreSQL、MongoDB …

js实现获取原生form表单的数据序列化表单以及将数组转化为一个对象obj,将数组中的内容作为对象的key转化为对象,对应的值转换为对象对应的值

1.需求场景 哈喽 大家好啊&#xff0c;今天遇到一个场景&#xff0c; js实现获取原生form表单的数据序列化表单以及将数组转化为一个对象obj&#xff0c;将数组中的内容作为对象的key转化为对象&#xff0c;对应的值转换为对象对应的值 数组对象中某个属性的值&#xff0c;转…

元宇宙现已开放!

在 2023 年 11 月 3 日 The Sandbox 首个全球创作者日上&#xff0c;The Sandbox 联合创始人 Arthur Madrid 和 Sebastien Borget 宣布元宇宙已开放&#xff0c;已创作完整体验的 LAND 持有者可以自行将体验发布至 The Sandbox 地图上。 精选速览 LAND 持有者&#xff1a;如果…

在JVM中 判定哪些对象是垃圾?

目录 垃圾的条件 1、引用计数法 2、可达性分析 3、强引用 4、软引用 5、弱引用 6、虚引用 判断垃圾的条件 在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;垃圾收集器负责管理内存&#xff0c;其中的垃圾收集算法用于确定哪些对象是垃圾&#xff0c;可以被回收…

VBA即用型代码手册之工作薄的关闭保存及创建

我给VBA下的定义&#xff1a;VBA是个人小型自动化处理的有效工具。可以大大提高自己的劳动效率&#xff0c;而且可以提高数据的准确性。我这里专注VBA,将我多年的经验汇集在VBA系列九套教程中。 作为我的学员要利用我的积木编程思想&#xff0c;积木编程最重要的是积木如何搭建…

[Latex] Riemann 问题中的激波,接触间断,膨胀波的 Tikz 绘图

Latex 代码 \begin{figure}\begin{subfigure}[b]{0.32\textwidth}\centering\resizebox{\linewidth}{!}{\begin{tikzpicture}\coordinate (o) at (0,0);\coordinate (Si) at (2.5,2.5);\coordinate (x) at (1,0);\draw[->] (0,0) -- (3,0) node[right] {$x$};\draw[->] …

ArkTS-自定义组件学习

文章目录 创建自定义组件页面和自定义组件生命周期自定义组件和页面的区别页面生命周期(即被Entry修饰的组件)组件生命周期(即被Component修饰的组件) Builder装饰器&#xff1a;自定义构建函数按引用传递参数按值传递参数 BuilderParam装饰器&#xff1a;引用Builder函数 这个…

生物动力葡萄酒和有机葡萄酒一样吗?

农业维持了数十万年的文明&#xff0c;但当人类以错误的方式过多干预&#xff0c;过于专注于制造和操纵产品时&#xff0c;农业往往会失败。如果我们的目标是获得最高质量的收成&#xff0c;并长期坚持我们的做法&#xff0c;我们就必须与土地打交道。 当我们开始寻找生物动力…

应用内测分发平台如何上传应用包体?

●您可免费将您的应用&#xff08;支持苹果.ios安卓.apk文件&#xff09;上传至咕噜分发平台&#xff0c;我们将免费为应用生成下载信息&#xff0c;但咕噜分发将会对应用的下载次数进行收费&#xff08;每个账号都享有免费赠送的下载点数以及参加活动的赠送点数&#xff09;&a…

【电路笔记】-分压器

分压器 文章目录 分压器1、概述2、负载分压器3、分压器网络4、无功分压器4.1 电容分压器4.2 感应分压器 5、总结 有时&#xff0c;需要精确的电压值作为参考&#xff0c;或者仅在需要较少功率的电路的特定阶段之前需要。 分压器是解决此问题的一个简单方法&#xff0c;因为它们…

【Vue】filter的用法

上一篇&#xff1a; vue的指令 https://blog.csdn.net/m0_67930426/article/details/134599378?spm1001.2014.3001.5502 本篇所使用指令 v-for v-on v-html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"&…

记一次docker服务启动失败解决过程

环境&#xff1a;centos 7.6 报错&#xff1a;start request repeated too quickly for docker.service 由于服务器修复了内核漏洞&#xff0c;需要重启&#xff0c;没想到重启后&#xff0c;docker启动失败了 查看状态 systemctl status docker如下图 里面有一行提示&…

网络互联与IP地址

目录 网络互联概述网络的定义与分类网络的定义网络的分类 OSI模型和DoD模型网络拓扑结构总线型拓扑结构星型拓扑结构环型拓扑结构 传输介质同轴电缆双绞线光纤 介质访问控制方式CSMA/CD令牌 网络设备网卡集线器交换机路由器总结 IP地址A、B、C类IP地址特殊地址形式 子网与子网掩…

DCDC电感发热啸叫原因分析

一、电感发热啸叫原因解析 发热原因&#xff1a;电感饱和&#xff0c;实际使用的电感值<理论电感计算值 原因1&#xff1a;电感选择过小&#xff0c;计算值不合理。 原因2&#xff1a;PCB布局不合理&#xff0c;屏蔽型电感下方应设禁止铺铜区。 啸叫原因&#xff1a; 人耳的…

Log4j2.xml不生效:WARN StatusLogger Multiple logging implementations found:

背景 将 -Dlog4j.debug 添加到IDEA的类的启动配置中 运行上图代码&#xff0c;这里log4j2.xml控制的日志级别是info&#xff0c;很明显是没生效。 DEBUG StatusLogger org.slf4j.helpers.Log4jLoggerFactory is not on classpath. Good! DEBUG StatusLogger Using Shutdow…

Camera Raw v16.0.0(PS Raw增效工具)

Camera Raw 16是一款允许摄影师处理原始图像文件的软件PS增效工具。原始图像文件是未经相机内部软件处理的数码照片&#xff0c;因此包含相机传感器捕获的所有信息。Camera Raw 为摄影师提供了一种在将原始文件转换为更广泛兼容的格式&#xff08;如 JPEG 或 TIFF&#xff09;之…

搭建SRS视频服务器

去官方网站下载FFmpeg6.1 https://ffmpeg.org/download.html拷贝到CentOS7.9中的/opt目录下&#xff0c;解压并重命名 tar -xvf ffmpeg-6.1.tar.xz 解压后编译安装 ./configure make make install从github下载SRS4.0release 解压后 如果ffmpeg的路径不在/usr/local/bin/ffmpe…