[Linux服务器] Linux之基础开发工具

162 0
Honkers 2025-4-26 06:18:25 | 显示全部楼层 |阅读模式


前言

本文主要介绍了Linux中的基础开发工具及其使用,具体包括yum(软件包管理器)、vim(编辑器)、gcc/g++(编译器)、makefile(项目自动化构建工具)、gdb(调试器)等。


一、yum(软件包管理器)

1. 什么是软件包?

我们在Linux下安装一个软件通常是下载程序的源代码,在本地进行编译,最终得到它的可执行程序。但是这样做比较麻烦,因此有一些人将常用的一些软件提前编译好做成软件包(相当于windows下的软件安装包)放在服务器上,通过软件包管理器可以直接获得这个编译好的软件包,在本地安装可执行程序。

2. 下载软件包

  1. 安装软件之前需要将软件的安装包下载下来
    注意,这一行为必须通过互联网。
  2. 软件包并不在我们的本地电脑,那么软件包在哪里呢?
    在别人的电脑上(远端服务器上,云服务器)。
  3. 那么问题来了,我们的电脑如何得知要去哪里找软件安装包呢?(我的电脑怎么知道我要下载的软件安装包在哪一台服务器上呢?)
    用现实中的例子,根据对象的不同,查找软件安装包的方式也不同:
    a.电脑:搜索软件官网;
    b.手机:手机的应用商店(当然,应用商店也只是手机上的一个app);
    c.Linux:软件包管理器(相当于应用商店)
  4. 由谁提供的软件包?又是由谁放在服务器上的?
    (1)企业、组织、个人为了获得某种利益提供了软件包;
    (2)提供了软件包后,一般将他们放在那里?
    a.电脑:软件官网
    b.手机:手机应用商店——提供商的服务器

3. yum

yum(Yellow dog Updater, Modified)是Linux下非常常用的一种软件包管理器。主要应用在Fedora, RedHat,Centos等发行版上。

3.1 验证主机网络

关于yum的所有操作必须保证主机(虚拟机)网络通畅,因此我们可以先通过ping命令验证当前网络是否通畅:

  1. ping www.baidu.com
复制代码

3.2 查看软件包

通过yum list命令可以罗列出当前有哪些软件包。因为软件包的数目会比较多,我们可以用grep命令筛选出我们所关注的软件包。

  1. yum list | grep lrzsz
复制代码

软件包名称:主版本号.次版本号.源程序发行号-软件包的发行号.主机平台.cpu架构。
"x86_64"后缀表示64位系统的安装包,“i686"后缀表示32位系统的安装包,选择安装包要与系统匹配。(操作系统的体系结构
"el7"表示操作系统发行版的版本。“el7”表示centos7/redhat7。“el6”表示centos6/redhat6。
base表示“软件源”的名称,类似于手机上的XXX应用商店。

3.3 安装软件

  1. sudo yum install [-y] <sl>
复制代码
  1. 使用yum命令需要sudo提权,因为软件包将会安装在特定的目录,该目录需要权限。如果是root用户,安装就不需要加sudo。
  2. yum会自动查找需要下载的相关安装包,此时输入"y"确认安装(如果指令加上-y则无需确认)。
  3. 出现"complete",说明安装成功。
  4. 其他要安装的软件包,我们要根据自己的需求进行下载安装。

举例:安装软件lrzsz
该软件可以实现windows和Linux的交互,使windows机器与远端的Linux机器可以通过xshell传输文件,安装完毕后可以通过拖拽的方式将windows中的文件传给Linux。

3.4 卸载软件

  1. sudo yum remove [-y] <sl>
复制代码
  1. 使用yum命令需要sudo提权,因为软件包是从特定的目录删除,该目录需要权限。如果是root用户,安装就不需要加sudo。
  2. yum会自动查找需要卸载的相关安装包,此时输入"y"确认卸载(如果指令加上-y则无需确认)。
  3. 出现"complete",说明卸载成功。

3.5 扩展内容(yum源)

3.5.1 查看yum源的配置文件
  1. ls /etc/yum.repos.d
复制代码

3.5.2 用其他yum源替换我们自己云服务器的yum源配置

如果使用linux环境是虚拟机,则需要将yum源替换为国内的镜像源(虚拟机的yum源是国外的,访问不稳定)。
1.备份原始的yum源(避免新的yum源有问题,导致不能恢复原始yum源的情况出现)

  1. mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS.repo_tmp
复制代码

2.下载阿里yum源配置文件

  1. wget https://mirrors.aliyun.com/repo/Centos-7.repo
复制代码

3.使用该yum源
将下载好的新yum源重命名为CentOS-Base.repo
当然如果使用的是云服务器就不需要更换yum源了(云服务器都是使用国内的镜像源)

3.5.3 非官方的软件集

官方软件集(centos、Ubuntu、kail等)中的软件都是经历长期使用确定稳定、安全、高效的。当然,也存在非官方的软件集合(yum源一般称为epel),其中的软件都是还在试用阶段的(之后有机会加入到官方)。
epel-release的安装(非官方扩展源)

  1. yum install -y epel-release
复制代码

安装非官方扩展源后,如果在安装软件时,没有在官方软件集中找到官方的下载链接,则yum会自动去epel-release中查找。

二、vim(编辑器)

1. vim的介绍

vim是Linux下一种功能强大,有多种模式的编辑器。
vim有三种常用模式,分别是命令模式插入模式底行模式

  • 命令模式:
    又叫做正常模式、普通模式、默认模式。进入vim后默认处于命令模式,可以转换为其他模式。可以控制屏幕光标的移动,字符、字或行的删除、移动、复制等。按ESC键即可从其他模式退出到命令模式
  • 插入模式:
    只有在insert mode下才可以进行文字输入,该模式是我们使用频繁的编辑模式,可以对文件中的内容进行正常编辑(像记事本一样)。按i即可进入
  • 底行模式:
    文件保存或退出,也可以进行文件替换(可以同时打开多个文件)、查找字符串、列出行号等操作。按:(同时按shift + ;)即可进入

2. vim的基本操作

2.1 进入vim

在系统的提示符号后输入vim及文件名称,默认进入vim全屏幕编辑画面

  1. $ vim test.c
复制代码

2.2 正常模式的操作

2.2.1 正常模式切换到插入模式

输入 a:从光标当前位置开始输入文字;
输入 i:从光标的所在位置的下一个位置开始输入文字;
输入 o:在文章刚开始插入新的一行,即,从文首开始输入文字。
从插入模式返回正常模式,按Esc

2.2.2 从正常模式切换到底行模式

输入:
从底行模式返回正常模式,按Esc

2.2.3 移动光标
  1. 按[h] [j] [k] [l]分别控制光标左、下、上、右移动
  2. 按[G]光标移动到文本的末尾
  3. 按[gg]光标移动到文本开始
  4. 按[$]光标移动到光标所在行的行尾
  5. 按[^]光标移动到光标所在行的行首
  6. 按[n + shift + g]光标移动到第n行
2.2.4 删除
  1. [x]每按一次。删除光标位置的一个字符
  2. [n + x]删除光标位置起的n个字符
  3. [X]删除光标所在位置的前一个字符
  4. [n + X]删除光标位置的前n个字符
  5. [dd]删除光标所在行
  6. [n + dd]删除光标所在位置起的n行
2.2.5 复制
  1. [yy]复制光标所在行到缓冲区
  2. [n + yy]复制从光标所在行起的n行到缓冲区
2.2.6 粘贴
  1. [p]将缓冲区的内容粘贴到光标所在位置
  2. [yy + p]复制粘贴
  3. [dd + p]剪切
2.2.7 替换模式
  1. [r]替换光标所在位置的一个字符(支持nr,将光标所在位置起的n个字符转化为一个字符)
  2. [shift + ~]将光标所在位置的字母进行大小写转化(一直按着可以自动转换到这一行结束)
  3. [R]替换光标所到之处的字符,直到按下Esc键
2.2.8 撤销上一次操作
  1. 如果误执行了一个命令可以按下回到上一个操作(将该命令撤销),按多次可以执行多次撤销操作。
  2. [ctrl + r]撤销的恢复(相当于撤销撤销这个操作)

2.3 底行模式的操作

首先确定您已经处于底行模式。

2.3.1 行号
  1. set nu/nonu
复制代码

列出行号/取消行号

2.3.2 分屏
  1. vs copy.c
复制代码

同时打开多个文件(代码中的copy.c是文件名),但是只有一个光标。光标的意义在于选中的行或屏幕。
光标跨屏:

  1. ctrl + ww
复制代码
2.3.3 执行shell指令

底行模式中,不需要退出vim,只需要在指令前加上!,即可强制执行shell指令。

  1. !gcc test.c -o test
复制代码

  1. !./test
复制代码

2.3.4 底行替换

将要替换的命令替换为替换后的命令。

  1. %/需要替换的命令/替换后的命令/g
复制代码

2.3.5 保存和退出文件
  1. wq
复制代码

单独的w是保存文件,单独的q是退出文件,两个一起是保存并退出文件。
如果文件提示无法退出,可以在命令的最前面加上!,强制退出。

3.vim的配置

没有进行配置的vim和记事本没有多大区别,使用起来比较困难。为了方便使用,我们要对vim进配置。
注意:vim的配置是一人一份的并不会互相影响,虽然大家用的是一个vim运行程序,但是大家的vim配置不同(每个成员在自己的家目录下有属于自己的vim配置)。

vim的配置是在家目录下的.vimrc文件中,可以在该文件中添加一系列的命令。因此,对vim进行配置实际上就是修改.vimrc文件。

三、gcc/g++(编译器)

1. 编译链接的过程

在正式介绍编译器之前,我们先回顾一下编译链接的过程。

2. gcc/g++的使用

2.1 预处理

预处理主要包括:头文件展开、宏替换、条件编译、去掉注释等工作。

  1. gcc -E test.c -o test.i
复制代码

从现在开始进行程序的翻译,直到预处理结束停下。
头文件展开:


去掉注释:

2.2 编译

编译主要包括:检查代码的规范性,以及是否存在语法错误等,确定无误后将代码编译为汇编语言

  1. gcc -S test.i -o test.s
复制代码

从现在开始,进程程序的翻译工作,直到编译结束文件变成汇编后,就停下来。

2.3 汇编

汇编主要包括:将汇编语言转换为二进制语言。(并非可执行程序,是二进制目标文件)

  1. gcc -c test.s -o test.o
复制代码

从现在开始,进行程序的翻译工作,做完汇编工作后,变成可重定向目标二进制文件,就停下来。


可以发现文件中存放的是我们看不懂的乱码,但是可以通过命令od test.o将这些乱码转化为我们可以看懂的01二进制文件

2.4 链接

链接的过程就是,将目标文件与标准库链接起来,形成一个可执行程序的过程

  1. gcc test.o -o test
复制代码

链接的过程形成了可执行程序(test是我们自定义的可执行程序文件名,可以进行修改)


可以看到整个过程中形成的所有文件:

3. 函数库

3.1 库函数

我们自己写的代码和库是两码事,我们自己写的程序中没有定义printf等函数的具体实现,且在预编译中包含的stdio.h中也只有这些函数的声明,只有在链接的时候该函数的实现才与我们的代码关联起来。那么这类函数是在哪里实现的呢?
答:这类函数都被实现在名为libc.so.6的文件中,在没有特别指向时,gcc会到系统默认的搜索路径/usi/lib下进行寻找。即,链接到libc.so.6库函数中去,这也是链接的作用,将库函数的实现与库函数的声明关联起来。
查看该文件:

  1. ls /lib64/libc.so.6 -l
复制代码

3.2 为什么会存在数据库?

为了让我们站在巨人的肩膀上,减少我们的开发成本。

4. 静态链接和动态链接

  1. gcc meytest.c -o mytest.s
复制代码

Linux下形成一个可执行文件使用的链接默认动态链接。如果要求是静态链接,就必须在命令尾部加上-static,如下:

  1. gcc meytest.c -o mytest.s -static
复制代码

4.1 动态链接

4.1.1 用一个小故事理解动态链接

从前,在XXX学校,有一个叫张三的学生,他每天的生活都很自律。每天都会严格执行自己的计划清单,清单内容:数学、语文、英语、上网、复习。他住在学校的宿舍里,身边有数学、语文、英语的学习资料,但是没有上网的工具。为了顺利执行计划,张三询问他的学长,学校附近是否存在网吧,学长告诉张三,出了学校北门往东走500米左右就到XXX网吧。因此张三先执行学习数学、语文、英语的计划,然后按照学长给他的地址到达XXX网吧进行上网,然后再回来复习今天的内容。
上面的故事可以参考下图进行理解:


如果将张三比作程序,则张三每日执行的计划清单就是程序员所写的代码,而其中上网这一项,则是程序要调用库函数。程序跳转到函数库中执行所需的库函数,执行完毕后再回到代码中继续向后执行。(这种调用库函数的方法称为动态链接)

4.1.2 问题
  1. 如果网吧升级或者被关闭会影响张三上网吗?
    会影响的,如果网吧要升级,它必然一段时间不开门,导致张三无法上网。更不用说是被关闭。
  2. 这样做耗费张三的时间吗?
    当然耗费,张三前往网吧上网,以及上网结束回到学校都要耗费时间的。
4.1.3 优点
  1. 动态链接形成的可执行的程序小;
  2. 节省资源——内存、磁盘;
  3. 方便下载。
4.1.4 缺点

会受到库升级或者库被删除的影响。

4.2 静态链接

4.2.1 用一个小故事理解静态链接

前几日由于受到学校的举报,学校附近的网吧都被关闭了。好在,张三的父母比较开明,也很信任自己的孩子,就给张三买下了张三常在网吧中学习的那台机子。在这之后,张三执行每日计划就不需要跑去外面的网吧,他在宿舍中就可以执行上网这一项内容,然后正常执行每日计划。


程序将所需的库函数从函数库中拷贝了一份,之后就不需要再跳转到函数库中执行该库函数了。(这种调用库函数的方法称为静态链接)

4.2.2 问题
  1. 如果网吧升级或者被关闭会影响张三上网吗?
    不影响,此时张三所需要进行上网的工具电脑,已经在张三身边。因此无论是网吧升级导致定制营业还是网吧彻底关闭都不会影响张三上网。
  2. 这样做耗费张三的时间吗?
    不耗费,电脑就在张三手边,和张三的语文英语等学习资料一样需要就可以拿来用,不需要在路上耗费时间。
4.2.3 优点

不受库升级或者库被删除的影响。

4.2.4 缺点
  1. 形成的可执行程序体积太大——网络、磁盘、内存。
  2. 同一个库函数可能会有多个C程序调用,此时使用静态链接就会导致代码冗余的问题。
4.2.5 静态链接拷贝的是.so内部的代码吗?

不是,如果需要进行静态链接的话,系统中就必须存在.a结尾的静态库。因为,动态链接找动态库,静态链接找静态库

5. Linux下库的命名

去掉前缀lib,去掉后缀.so 剩下的就是库名称。动态库的后缀是.so,静态库的后缀是.a。
eg: libC.so 是 C标准库

5.1 动态库

libXXXX.so

  1. 一般而言,系统会自带动态库(为什么?
    因为系统的运行需要用到动态库。
  2. 敢不敢删除系统中的C动态库?
    不敢。使用C动态库的程序非常多,而库只有一份,所有用C语言编写的程序不会出现重复的库代码!因此,将动态库又称为共享库
  3. 如果我们下载了一个用C编写的程序,还需要额外下载C标准库吗?
    不需要,它会自带。

5.2 静态库

libYYYY.a

  1. 静态库如果不存在,则需要自己安装(如果要生成静态链接,需要自己先安装C/C++静态库)。
  2. 如果系统中只存在静态库,则默认的链接方式就是静态链接
  3. 敢不敢删除系统中的C静态库?
    原因同动态库。

6. 总结

  1. 系统本身为了支持我们编程,给我们的程序提供了什么呢?
    系统给我们提供的标准库.h(告诉我们怎么用),标准的动静态库.so/.a(告诉我们,方法的具体实现我有,有需要就来找我)。
    我们的代码 + 库的代码 = 可执行程序
  2. 上面的内容只在Linux下有效吗?
    windows下原理是一样的(windows中动态库.dll,静态库.lib)。同时windows下默认的链接方式也是动态链接。

四、makefile(自动化构建工具)

makefile是一个文件,make是一个命令(执行makefile中内容的命令)。

1. makefile原理

makefile存在的意义,是为了构建项目(做的一件事情),而做一件事情需要:a.依赖关系,b.依赖方法
依赖关系,用哪个源文件生成目标文件;
依赖方法,如何用源文件生成目标文件。

  1. 举个生活中的例子:
    你去向你爸要生活费,会说爸,给我点生活费。这件事中的依赖关系是“你和你爸是父子关系”;“依赖方法是你爸给你打钱”。
    因此,makefile内部要保存大量的依赖关系和依赖方法。
  2. 运用到代码里的例子:

    第一行是依赖关系(mytest文件的形成依赖mytest.c文件),第二行是依赖方法(mytest.c文件经过编译形成mytest文件)
    注意:第二行的最开始空白是一个Tab而不是几个空格。

2. makefile的语法

  1. make #会执行第一个目标文件的生成语句
  2. make <目标文件文件名> #会执行指定文件的生成语句。
复制代码

3. 如何确保目标文件每次是否需要重新编译生成?

根据目标文件和它依赖的文件的修改时间差(关于时间的具体内容参见下文):如果它所依赖的文件修改时间在目标文件修改时间之前,则不需要再进行重新生成目标文件;如果它所依赖的文件修改时间在目标文件修改时间之后,则需要再重新生成目标文件。
一旦目标文件被设置为伪目标文件,即用.PHONY:进行修饰,则目标文件每一次都将无视修改时间,即必须重新生成。

4. makefile的推导规则

如果要将上面例子的每个步骤都进行执行的话,则需要如下图的代码:


make在缺省情况下(不指定目标文件),会只执行第一个目标文件的代码。如果第一个目标文件所依赖的文件还没有生成,就会去生成它所依赖的文件,然后再生成目标文件(这一过程类似栈的规则:先进后出)。

五、gdb(调试器)

1. 背景知识

  1. 程序的发布方式有两种:debug版本和release版本。

debug版本方便程序员对程序进行调试,而release版本将很多可以调试的地方优化了(例如:assert会在release版本下失效)。
release的大小也比debug大。

  1. Linux gcc/g++得到的二进制程序默认是以release版本进行发布的。
  2. 如果希望能够调试程序,就需要将程序以debug版本进行发布。(在源代码生成二进制程序的命令中加上-g选项)

2. 使用(指令)

2.1 安装

  1. sudo yum install -y gdb
复制代码

2.2 发布debug版本的文件

  1. gcc -g test.c -o test_g
复制代码

默认情况下gdb无法对当前发布的版本(默认为release版本)进行调试,要发布debug版本才行。

2.3 相关的调试指令

发布debug版本的文件,使用调试指令进行调试。

list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。
list/l 函数名:列出某个函数的源代码。
r或run:运行程序。
n 或 next:单条执行。
s或step:进入函数调用
break(b) 行号:在某一行设置断点
break 函数名:在某个函数开头设置断点
info break :查看断点信息。
finish:执行到当前函数返回,然后停下来等待命令
print(p):打印表达式的值,通过表达式可以修改变量的值或者调用函数
p 变量:打印变量值。
set 变量:修改变量的值
continue(或c):从当前位置开始连续而非单步执行程序
run(或r):从开始连续而非单步执行程序
delete breakpoints:删除所有断点
delete breakpoints n:删除序号为n的断点
disable breakpoints:禁用断点
enable breakpoints:启用断点
info(或i) breakpoints:参看当前设置了哪些断点
display 变量名:跟踪查看一个变量,每次停下来都显示它的值
undisplay:取消对先前设置的那些变量的跟踪
until X行号:跳至第X行
breaktrace(或bt):查看各级函数调用及参数
info(i) locals:查看当前栈帧局部变量的值
quit:退出gdb

2.4 部分调试指令的演示

注意:gdb中如果本次没有输入指令,则默认同上一次的指令相同,直接进行执行(直接enter)。

  1. l
    显示代码(list的简称,l 0从第一行开始显示,继续enter会继续显示剩余的代码)
  2. b
    打断点(b 是break的简称,即break后跟要打断点的代码行即可)
  3. info b
    查看断点
  4. d +断点编号
    取消断点
  5. r
    调试运行,到第一个断点处停下(run的简称)
  6. n
    逐过程调试(next的简称,该调试方式不进入函数)
  7. s
    逐语句调试(step的简称,该调试方式进入函数)
  8. bt
    显示函数调用堆栈
  9. fin
    直接将函数运行完(finish的简称)
  10. display
    显示变量(类似于vs2013中的监视)
    test.c的代码:

    调试现象:

六、Linux中文件的三个时间

1. access

被访问的时间,原本的规则是每次访问时间都会被改变,但是由于一旦文件被修改,就意味着它被访问了,而它被访问了却不一定被需修改,为了避免修改频率太快,导致资源浪费(访问时间用处也不大),因此是在一段时间里,多次访问才会更新时间(不一定是最新时间)。

2. modify

文件内容的修改时间(最新时间)

3. change

文件属性的修改时间(最新时间),文件内容发生变化会导致文件属性发生修改,文件属性包括文件内容

七、简单写一个倒计时

1. 缓冲区问题

看看如下代码

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. int
  4. main() {
  5. printf("Hello World!");
  6. sleep(10);
  7. return 0;
  8. }
复制代码
  • 先sleep还是先printf?
    根据程序运行的顺序,先printf再sleep。
    但是,通过运行我们发现是先sleep了,之后才打印出来了。
    说明是先将内容放在缓冲区,sleep后才将缓冲区内容显示在屏幕上。
    但是,如果在printf中增加\n,则会及时的显示内容
  • 因此,我们明白 printf 的内容是先存放在缓冲区里,等到缓冲区刷新时才会显示到显示器上

2. 回车换行

一般而言:

  • \r换行是指换到下一行;
  • \n回车指回到当前行的最开始;

但是,在语言层面:\n就是回车+换行

3. 显示器为什么能显示各种符号?

显示器面板上有各种像素点,凡是显示到显示器上的都是字符(显示器不认识字符的含义,只是根据用户的要求进行显示一个一个的字符)

4. 倒计时代码

代码如下,大家可以自行运行尝试。

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. int
  4. main() {
  5. int cnt = 10;
  6. while(cnt) {
  7. printf("剩余时间:%2d\r", cnt--);
  8. fflush(stdout);
  9. sleep(1);
  10. }
  11. printf("\n");
  12. return 0;
  13. }
复制代码

注意:

  1. 用\r回到当前行的最开始,用新的值覆盖旧的值就能实现动态倒计时的效果;
  2. 因为\r不会主动刷新缓冲区,导致缓冲区的内容无法显示在显示器上,因此每一次都要用函数fflush对缓冲区进行刷新
  3. 因为cnt会从两位数变为一位数的,我们为了将它最初的两位数都能进行覆盖,就要对打印的格式进行控制。

八、Linux下的第一个小程序——进度条

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #define NUM 4
  4. #define N 1 //控制进度条图案
  5. void
  6. procession() {
  7. char stlye[NUM] = {'+', '-', '*', '>'}; //可选进度条图案
  8. char a[] = "\|/-"; //转动图案变化
  9. int cnt = 0;
  10. char arr[102] = {'\0'};
  11. while (cnt <= 100) {
  12. printf("[%-100s][%d][%c]\r", arr, cnt, a[cnt%4]);
  13. arr[cnt++] = stlye[N]; //进度条增长
  14. fflush(stdout); //刷新缓冲区的内容到屏幕上,否则usleep后才能打印出来
  15. usleep(50000);
  16. }
  17. printf("\n");
  18. }
  19. int
  20. main() {
  21. procession();
  22. return 0;
  23. }
复制代码

总结

以上就是今天要讲的内容,本文介绍了Linux中的基础开发工具的相关知识。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Honkers

特级红客

关注
  • 3159
    主题
  • 36
    粉丝
  • 0
    关注
这家伙很懒,什么都没留下!

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行