基于自定义段技术构建指令系统

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

1 自定义段

1.1 定义段

gcc支持自定义段。格式如下:

/* my_section为自定义段名 */
__attribute__((section(my_section))) 

使用自定义段修饰的变量或函数在编译链接时会被放入指定的段中。

例如:

/* global_var会被放到my_section段 */
__attribute__((section(my_section))) int global_var = 1; 
/* fun函数会被放到my_section段 */
__attribute__((section(my_section))) int fun(void){}

Tips:

  1. 本文中介绍的是基于Linux系统和gcc编译器自带的lds新增自定义段。
  2. 基于第一点,对于自定义的段名是有限制的——不允许以”."开头,因为以“."开头的段都是系统保留的,例如.txt和.data等。
  3. 使用自定义段时请不要再自己定义__GNUC__宏,否则自定义段不生效。

1.2 引用段

# 使用readelf命令查看符号表(option是小s)
readelf -s help_elf

结果如下:

54: 00000000006010fc     0 NOTYPE  GLOBAL DEFAULT   26 __stop_help_cmd
67: 0000000000601034     0 NOTYPE  GLOBAL DEFAULT   26 __start_help_cmd

我的代码中自定义的段名是help_cmd,编译器会自动生成该段起始地址变量(起始地址变量这个名字很奇怪,后面解释)和结束地址变量。

需要注意的是 &__start_help_cmd 才是段help_cmd的起始地址,而 __start_help_cmd只是一个变量,变量类型在extern时指定。如下例:

/* 声明编译器自动生成的段起始地址对应的变量,其中cmd_tbl_t是一个结构体类型 */
extern cmd_tbl_t __start_help_cmd;
/* 注意下面这种声明是错误的(因为存放时存放的是cmd_tbl_t类型的变量,不是指针) */
extern cmd_tbl_t *__start_help_cmd;

另外,还需要说明的一点是,&__stop_help_cmd对应的地址是自定义段help_cmd结束后的下一个字节对应的地址(和对齐无关)。


2 段的对齐属性

在调试该功能过程中遇到段错误问题,后来定位发现是段对齐属性导致。所以,自定义段时一定要同步设置段的地址对齐属性,防止出现段错误问题,或者更换编译器、系统之后出现问题。

下面,通过一个例子来说明段对齐属性的重要性。

2.1 默认对齐

我一开始使用的是“标准”的自定义段的方式定义的变量,代码如下:

/* 自定义段:help_cmd */
#define Struct_Section  __attribute__ ((unused,section ("help_cmd")))
#define HELP_ADD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __help_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
/* 定义两个变量到自定义段 */
HELP_ADD(dabaoxin,1,1,2,"I like playing basketball","help");
HELP_ADD(dabaoge,1,1,2,"I like eatting ","help");

其中,cmd_tbl_t为我自定义的结构体类型(在下一节中会展开介绍)。当检索第一个指令dabaoxin时可以正确访问该结构体内的成员。问题就出在访问第二个指令dabaoge所在结构体时。因为,使用指针自加操作时,找到的并不是第二个结构体的基地址,从而造成段错误。到这里,没明白我说什么不重要,请继续往下看~ 真相会逐渐浮出水面~

为了更清楚的说明这个问题,我们来看下编译出的elf文件的内部结构(基础知识请参考《程序员的自我修养》这本书)。

首先,我们使用下述指令看一下段表内的信息。我们发现help_cmd段的Align参数默认居然是32个字节。也就是说在我们自定义的段help_cmd中每个成员的首地址必须是32的整数倍。

# 读取elf段表信息(option是大S)
readelf -S help_elf 
 ! 默认对齐属性的段表信息
 Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
       
  [25] .data             PROGBITS         0000000000601030  00001030
       0000000000000004  0000000000000000  WA       0     0     1
  [26] help_cmd          PROGBITS         0000000000601040  00001040
       0000000000000128  0000000000000000  WA       0     0     32
  [27] .bss              NOBITS           0000000000601168  00001168
       0000000000000008  0000000000000000  WA       0     0     1

当时,在定位问题时,为了更清楚的看到自定义段help_cmd的地址和内容,使用了反汇编指令,指令如下。

我们可以看到,在段help_cmd中存放一个cmd_tbl_t结构的变量居然占用了64个字节,而一个cmd_tbl_t的真正大小只有40个字节(通过sizeof关键字或者readelf -s(小s)指令都可以确认这一点)。

所以,如果我们定义一个cmd_tbl_t类型的指针p指向 &__start_help_cmd的话,p++指向的地址是0x601068,并不是我们期望的0x601080。而0x601068的内容全部是空,如果结构体内的成员是指针类型的话必然会产生空指针解引用,从而导致段错误。

那如何解决这个问题呢?

答案是在自定义段的同时声明地址对齐方式。

# 获取反汇编完整信息(option是小s)
objdump -s help_elf
! 32字节对齐反汇编结果
Contents of section help_cmd:
 601040 10074000 00000000 01000000 01000000  ..@.............
 601050 02000000 00000000 19074000 00000000  ..........@.....
 601060 33074000 00000000 00000000 00000000  3.@.............
 601070 00000000 00000000 00000000 00000000  ................
 
 601080 38074000 00000000 01000000 01000000  8.@.............
 601090 02000000 00000000 40074000 00000000  ........@.@.....
 6010a0 33074000 00000000 00000000 00000000  3.@.............
 6010b0 00000000 00000000 00000000 00000000  ................
 
 6010c0 50074000 00000000 01000000 01000000  P.@.............
 6010d0 02000000 00000000 40074000 00000000  ........@.@.....
 6010e0 33074000 00000000   				 3.@.....


2.2 指定对齐

在自定义段的同时指定对齐方式的格式如下:

/* 自定义段的同时指定段内元素地址对齐方式为:以4字节对齐 */
__attribute__ ((unused,section ("help_cmd"), aligned(4)))

再来看看指定段help_cmd以4字节对齐之后的段表信息和反汇编结果。我们发现,段表信息中help_cmd的Align属性已经变成了4字节。

在反汇编结果中我们也发现,元素间是 “紧凑”存放的了,这样先前定义的 p指针在进行自加操作后便可以精准指向第二个结构体变量元素了。段错误问题完美解决~~

 ! 修改对齐属性后的段表信息
 Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align

  [25] .data             PROGBITS         0000000000601030  00001030
       0000000000000004  0000000000000000  WA       0     0     1
  [26] help_cmd          PROGBITS         0000000000601034  00001034
       00000000000000c8  0000000000000000  WA       0     0     4
  [27] .bss              NOBITS           00000000006010fc  000010fc
       0000000000000004  0000000000000000  WA       0     0     1
! 修改对齐属性后的反汇编结果
Contents of section help_cmd:
 601034 20074000 00000000 01000000 01000000   .@.............
 601044 02000000 00000000 29074000 00000000  ........).@.....
 601054 43074000 00000000 48074000 00000000  C.@.....H.@.....
 601064 01000000 01000000 02000000 00000000  ................
 601074 50074000 00000000 43074000 00000000  P.@.....C.@.....
 601084 60074000 00000000 01000000 01000000  `.@.............
 601094 02000000 00000000 50074000 00000000  ........P.@.....
 6010a4 43074000 00000000 


3 指令注册

我们最终要达到的效果是这个样子:添加一个帮助指令,在填写函数头的过程中就完成注册。

函数头的格式如下:

/* HELP_ADD 使用举例 */
HELP_ADD(
	fun,								/*function name*/
	"测试函数",							 /*brief*/
	3,									/*the number of parameters*/
	"u32 u32i:通道号(取值范围0-15)",	/*parameters*/
	"u8  u8j :栅压点",
	"u32 u32k: 电压值(单位mV)");

下面让我们剖析实现细节。


来不及解释了,先上代码。详细解释请参照代码注释和后续叨叨~

/* help.h 文件内容 */
#ifndef __HELP_H__
#define __HELP_H__
/* 为了提高搜索速度定义的编译器优化选项(借鉴Linux)*/
#define likely(x)	__builtin_expect(!!(x), 1) 
#define unlikely(x)	__builtin_expect(!!(x), 0)
/* 基于位段做的“宏定义在编译阶段的入参检查方法” */
#define SUPPORT_MAX_ARGS 5
#define CHECK(name,x)	struct __check##name{unsigned:(-!!(x));};
/* help指令数据结构定义 */
#pragma pack(4)
typedef struct {
	char	*name;		/* Command Name			*/
	char	*brief;		/* Usage brief message	(short)	*/
	int	    argc;		/* maximum number of arguments	*/
	char	*argv[SUPPORT_MAX_ARGS];	/* Help  message of each arguments (long) */
}cmd_tbl_t;
#pragma pack()
/* 自定义段 */
#define Struct_Section  __attribute__ ((unused,section ("help_cmd"), aligned(4)))
/* help指令添加宏方法定义 */
#define HELP_ADD(name,brief,argc,args...) \ /*args使用可变参数列表*/
CHECK(name, argc>SUPPORT_MAX_ARGS);	  \		/*在编译阶段检查入参,防止指令检索时越界访问造成任务异常*/
cmd_tbl_t __help_cmd_##name Struct_Section = {#name, brief, argc, {args}}

4 指令检索

4.1 help搜索

help搜索想达到的效果是给定记忆中的关键词(全部或者部分),然后直接跟在help后面就可以将相关指令格式化输出。

搜索实现的关键代码如下:

下面是help指令的实现,主要是在某些情况下调用help_usage函数来提示使用方法;调用find_cmd函数来查找用户输入的关键字,并将关键字匹配的所有指令整齐地打印出来。

/* help搜索实现 */
extern cmd_tbl_t __start_help_cmd;
extern cmd_tbl_t __stop_help_cmd;

int help(char *argv)
{
	int i = 0;
	cmd_tbl_t *pstr =  &__start_help_cmd;
	char *format_str = NULL;

    if (NULL == argv){
        return help_usage();
    }
    
    format_str = argv;
    printf("[debug] format_str = %s .\n", format_str);

    /* print usage */
    if ('?' == *format_str){
        return help_usage();
    }

    /* find cmd */
	do{
		pstr = find_cmd(format_str, pstr);
		if (NULL != pstr){
			printf("----------------------------------------------------------------------------\n");
			printf("|| name  \t| %s \n",pstr->name);
			printf("|| brief \t| %s \n",pstr->brief);
			for (i = 0; i < pstr->argc; i++){
				if (pstr->argv[i])
					printf("|| @ para%d\t| @ %s \n", i, pstr->argv[i]);
			}
			printf("----------------------------------------------------------------------------\n");
			pstr++;
		}
	}while(pstr); 
	
	return 0;
}

下面是搜索相关的代码实现,为了较好地提升搜索的速度和简便地实现大小写忽略,没有直接使用strstr库函数,而使用了sunday搜索算法。代码如下:

find_cmd函数实现,主要功能是在自定义段中搜索与关键字匹配的指令结构体并返回。当前只支持基于命令名称的英文搜索,该函数可扩展成支持基于命令brief的中文搜索(如果你的指令说明是中文的话可能用起来更亲民一些)。

cmd_tbl_t *find_cmd (const char *cmd, cmd_tbl_t *cmd_start_p)
{
	cmd_tbl_t *cmdtp = cmd_start_p;
	int offset = -1;

	for (;cmdtp != &__stop_help_cmd; cmdtp++){ 
		if ((offset = sunday_searchi(cmd, cmdtp->name)) >= 0){
			return cmdtp;
		}
	}
	
	return NULL;	/* not found */
}

下面是sunday算法的实现,据说大部分文本的搜索都是使用的该种搜索算法,既简单又高效。

static inline int sunday_searchi(const char * const format, const char * const raw)
{
    int format_len = 0;
    int raw_len = 0;
    int offset = -1;
    int i = 0;
    const char *pr = raw;

    if (unlikely(NULL == format || NULL == raw)){
        printf("%s: NULL pointer fault!\n", __FUNCTION__);
        return -1;
    }

    format_len = strlen(format);
    raw_len = strlen(raw);
    if (unlikely(format_len > raw_len)){
        return -1;
    }

    while(pr -raw + format_len <= raw_len){
        /* compare(ignore case) */
    	for (i = 0; i < format_len; i++){
    		if ((format[i]|0x20) != (pr[i]|0x20)) /*忽略英文大小写*/
    			break;
    		if (format_len-1 == i)
    			return pr - raw;
    	}
	
        /* move */ /*该算法的核心就在于此处的指针移动*/
        offset = find_last_chari(*(pr + format_len), format);
        pr += format_len - offset;
    }

    return -1;
}
static inline int find_last_chari(const char c, const char * const raw)
{
    const char *prtail = raw + strlen(raw) -1;

    if (unlikely(NULL == raw)){
        printf("%s: NULL pointer fault!\n", __FUNCTION__);
        return -1;
    }

    do{
        if ((*prtail|0x20) == (c|0x20))
                return prtail - raw;
        prtail--;
    }while (prtail >= raw);

    return -1;
}

4.2 help ?

为了更加人性化,可以在用户只输入help或者 help ?时给出一定提示信息,比如使用方法或者一些常用关键字。

int help_usage(void)
{
    int i = 0;
    printf("\n--------------------------------------------------\n");
    printf("|| 使用方法\t\t| rr + 关键字(关键字可简写) \n");
    printf("--------------------------------------------------\n");

    printf("--------------------------------------------------\n");
    printf("|| 关键字\t\t| 含义 \n");
    printf("--------------------------------------------------\n");
    for (; i < sizeof(stHelpUsage)/sizeof(help_usage_t); i++)
    {
        printf("|| %-10s\t| %s 相关指令 \n", stHelpUsage[i].englishName, stHelpUsage[i].chineseName);
        printf("--------------------------------------------------\n");
    }
    
    return 0;
}

关键字结构体定义

typedef struct{
    char *englishName;
    char *chineseName;
}help_usage_t;

help_usage_t stHelpUsage[] = {
    {"ver",     "版本查询"}
    /* add here in module */
};

<完>

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
『红蜘蛛多媒体网络教室』软件产品由广州创讯软件有限公司开发,简称《红蜘蛛软件》,上市已经超过12年。该软件运行于Windows2000/XP/2003/Vista/Windows7/Windows8网络,同时支持32位和64位Windows系统,主要在局域网络上实现多媒体信息的教学广播,是一款实现在电子教室、多媒体网络教室或者电脑教室中进行多媒体网络教学的非常好的软件产品,集电脑教室的同步教学、控制、管理、音视频广播、网络考试等功能于一体,并能同时实现屏幕监视和远程控制等网络管理的目的。它专门针对电脑教学和培训网络开发,可以非常方便地完成电脑教学任务,包括屏幕教学演示与示范、屏幕监视、遥控辅导、黑屏肃静、屏幕录制、屏幕回放、VCD/MPEG/AVI/MP3/WAV/MOV/RM/RMVB等视频流的网络播放、网络考试和在线考试、试卷管理和共享、网上语音广播、两人对讲和多方讨论、语音监听、联机讨论、同步文件传输、提交作业、远程命令、电子教鞭、电子黑板与白板、电子抢答、电子点名、网上消息、电子举手、获取远端信息、获取学生机打开的程序和进程信息、学生上线情况即时监测、锁定学生机的键盘和鼠标、远程开关机和重启、学生机同步升级服务、计划任务、时间提醒、自定义功能面板、班级和学生管理等,对于传统的教学模式来说,这是一种教学上的突破。 《红蜘蛛软件》上市超过12年,在数以万计的学校或企事业单位成功应用,并顺利进入新加坡、马来西亚、香港、澳门、台湾等国家或地区的市场,得到广大用户的好评,尤其是其优异的速度、稳定性与突出的性能。 传统的电脑教室,一般辅以投影仪或硬件网络系统,建立多媒体网络教室,但基于成本高昂和一些其他的原因,并不是一般教室可以配备的,而且硬件设备耗损大、维护烦琐、升级麻烦都是令学校困扰的问题。而『红蜘蛛多媒体网络教室』软件作为一种纯软件的解决方案,完全避免了这些硬件问题,而且教师可以把理论教学与实践操作相结合,直接在教师机上进行各种教学演示,并把每一步操作过程都实时同步传送到学生的电脑屏幕上。各种大量的多媒体课件资料、光盘教学资源、实验演示系统、教学方法与经验等都可以借助『红蜘蛛多媒体网络教室』软件这种系统实现了集语音、图像、文字、动画于一体的现代交互式教学模式。可以在整个多媒体教室里共享文字、图像、语音、视频资源。这样,通过构建一种文字、语音、视频图像的互动交流环境,学生可以同时在自己的电脑屏幕上分享各种教学资源,不但大大减轻了教师的工作负担,极大地提高了教学效率,而且也使教学内容极其生动活泼,学生乐于接受。 杰出优势: 首款全面兼容Windows XP、Windows 7、Windows 8的广播教学软件,并且同时支持32/64位系统; 采用全新视频驱动核心、MMX/SSE/SSE2指令和多级缓存技术,极大地提高屏幕广播速度和性能,对3D/游戏/电影/多媒体课件/动画/DVD视频/FLASH/POWERPOINT等都能非常流畅地没有任何延迟地进行广播,达到每秒30帧的速度; 支持DirectDraw、Direct3D、OpenGL、Layered Window等各种2D/3D窗口画面; 禁止Ctrl+Alt+Del、学生迟到或学生机重启后自动被控制等多种防止学生机“逃脱”的机制; 学生上线、未上线、退出、异常退出或逃脱、网络掉线等各种上线情况的即时检测; 捆绑一般电子教室软件都没有的网络考试和在线考试系统,实现自动评分的无纸化考试; B/S结构的考试系统,出卷和考试都在浏览器上完成,所有用户之间还能共享和交换试卷; 用户可以使用软件厂商搭建的考试服务器,无须自行安装和维护,就可以完全使用网络考试服务; 非常突出的系统稳定性,优异的速度和性能,广受用户好评; 纯软件架构,占用空间小,安装简单,升级维护方便; 功能完善,细节周详,界面直观、简洁、美观、标准化; 积累大量用户的使用经验,实用成熟; 灵活易用、管理方便; 先进的技术与理念,造就优秀的软件。
本文首发于DF创客社区,作者:不脱发的程序猿 【脑洞大赛】基于NB-IoT的智慧路灯监控系统(项目简介) 项目背景 每当夜幕降临,城市中各种各样、色彩缤纷的路灯亮起,为城市披上了一层绚丽的外衣。但在这绚丽的外表下则隐藏着巨大缺点: 1)能源浪费:由于城市的夜晚进入后半夜后,人们已经开始休息,街上人流量开始减少,有些地在特殊时根本不需要过多的路灯照明,导致能源浪费,增加了不必要的成本; 2)维护困难:由于使用人工巡检,需要大量人力,而路灯数量庞大,路灯实时状态不能及时获取,导致路灯故障维护、排查效率极低。 需求分析 路灯管理平台的建设,是智慧城市的一个重要组成部分,不仅能够实现城市及市政服务能力提升,也是智慧城市的一个重要入口,可促进“智慧市政”和“智慧城市”在城市照明业务方面的落地。以往在路灯管理上存在着很多问题,例如路灯开关、巡查、维护基本靠人力,重点地晚上需要派人巡视,以保证设备的齐备率;路灯发生故障时,检修人员无法确定路灯精确位置;管理人员对路灯无法进行分时控制、无法监控路灯整体状态;路灯保持常亮状态效率低、不节能。针对以上问题,本系统设计的智慧路灯监控系统结构图如下所示: 具体功能如下: 1)路灯节点支持自定义控制方式,可支持自定义时间控制策略和多样化控制(两侧路灯全亮、全关、隔杆高亮等)两种方式。 2)根据所在环境光照强度,自动调节路灯亮度,低功耗节能减排。 3)断电保护,电压电流超过安全阈值,路灯自动断电。 4)路灯故障自动报警,GPS精确定位,可从手机APP、微信小程序、PC端和Web平台可视化监控路灯信息,随时可调取任何一处路灯信息。 5)实时采集路灯节点工作状态、电压、电流、功率、功率因数、耗电量、产生二氧化碳、频率、环境光照度和路灯状态数据,实现统计分析和历史查询。 6)根据路灯节点历史数据,使用机器学习算法,分析路灯使用状况,构建精准城市路灯画像。 总之,基于NB-IoT技术的城市道路智慧路灯监控系统有着广阔的前景和宽广的需求。 功能设计 基于NB-IoT技术的城市道路智慧路灯监控系统,在每个照明节点上安装一个集成了NB-IoT模组的单灯控制器,单灯控制器再经运营商的网络,与路灯控制平台实现双向通信,路灯控制平台直接对每个灯进行控制,包括开关灯控制、光照检测、自动调节明暗、电耗分析等操作。智慧路灯实物图如下所示: 与传统“两跳”方案不同,基于NB-IoT技术的解决方案不需要网关,每个NB-IoT路灯控制器直接接入运营商的NB-IoT网络,即可与控制平台通信,如下图所示。 基于NB-IoT技术的城市道路智能路灯监控系统包括感知层、网络层和应用层。 感知层由单独的路灯控制模块和NB-IoT终端构成。道路上的每个路灯都安装1个路灯控制模块,路灯控制模块管理路灯的开关、负责数据信息的采集和监控路灯的运行状态,它通过NB-IoT网络与NB-IoT终端进行无线通信;NB-IoT终端将路灯控制模块采集的数据信息上传到NB-IoT基站,将应用层中的手机或监控中心的管理命令下达到路灯控制模块,对感知层的路灯进行管理和监控,使用AI技术赋能,构建精准城市路灯使用状况画像分析。网路层由NB-IoT基站和Internet网络构成。Internet网络主要应用4G的LTE平台,将感知层的数据信息实时地传送到应用层,同时将应用层的控制命令传送到感知层。智能路灯监控系统网络架构如下图所示。 本系统设计方案具有以下优势: 1)对路灯的控制上,采用分时的3种控制策略,可以实现分时间控制道路两侧路灯全亮模式、自动调整模式(根据环境光照强弱或电压电流阈值)、终端联控模式,在满足照度需求的情况下,实现对电能的节省。 2)通信方式所采用的是中国电信的NB-IoT网络,拓扑简单、部署成本低,NB-IoT采用DRX模式,实现终端的实时在线,这种通信方式更适合静止的和低移动性且需要下发指令的场景。 3)利用GPS地理信息管理系统,可以在手机APP、微信小程序PC应用和Web平台界面上直观定位每个路灯的位置,便于维修人员确定故障路灯地址、及时维修。 4)高精度数据采集与通信技术,可采集路灯节点工作状态、电压、电流、功率、功率因数、耗电量、产生二氧化碳、频率、环境光照度和路灯状态数据,数据精度达到小数点后两位。 5)智能化设计,使用机器学习算法,实现对路灯使用状态的分析和评估。 本项目目前已经完全实现,演示视频如下所示: 基于NB-IoT的智慧路灯监控系统(设备选型)基于NB-IoT的智慧路灯监管系统在感知层可实现实时采集路灯节点的工作状态(亮灭状态)、电压、电流、功率、功率因数、耗电量、产生二氧化碳、频率、环境光照度、路灯亮度、路灯故障地理位置11种传感数据信息。 路灯控制终端节点主要由主控制器、NB-IoT无线通信模块、GPS模块、光强检测

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值