2018-05-28 16:16:25 qq_33973210 阅读数 207
  • 韦东山嵌入式Linux第一期视频

    掌握写汇编代码的能力,可以分析任意裸板包括U-boot、内核里的相关汇编代码; 掌握常用的硬件部件的操作,比如GPIO,UART,I2C,LCD,触摸屏; 深入理解ARM体系统架构,可以写出具备中断功能的裸板程序,对程序现场的保存、恢复有所了解,这些原理适用于任何架构CPU,适用于内核及应用程序; 熟悉裸板程序的结构,给你一个Bootloader就能分析它的初始化部分、内存使用情况、链接地址、重定位、程序的相对跳转/绝对跳转等;掌握Bootloader启动内核的原理,能完全自己写出一个Bootloader。

    234477 人正在学习 去看看 韦东山

中断简介

Linux0.11使用的Intel i386芯片共有256个中断,表现为中断号0~255.

其中前0~31号中断已经由Intel预定义,其余中断号为可编程中断。

32~47号分别对应linux的16个硬件中断信号(包括时钟、键盘、软盘等)。

0x80中断即128号中断为linux系统调用软中断。


硬中断和软中断

  • 软中断是执行中断指令(int n,n为中断号)主动产生的。而硬中断则是由外部引发的,具有随机性、突发性
  • 硬中断中断号由中断控制器提供,而软中断中断号直接由指令指出。


系统调用

系统调用int 0x80是一个软中断,是应用程序与Linux内核交互的接口。

系统调用初始化过程

在init/main.c初始化主程序main()中调用的sched_init()调度初始化函数中最后一行

set_system_gate(0x80,&system_call);

将0x80软中断与系统调用入口函数system_call联系起来。

---------------------------------------------------------------------------------------------------------------------------------------------

init/main.c : main() : line-132 --> kernel/sched.c : sched_init() --> set_system_gate(0x80,&system_call);

------------------------------------------------------------------------------------------------------------------------------------

set_system_gate()

为include/asm目录中system.h文件中的宏函数

#define set_system_gate(n,addr) \
	_set_gate(&idt[n],15,3,addr)
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))

该函数即为设置0x80号中断的中断描述符,附陷阱门描述符结构


  • DPL为优先级,3为用户态,0为内核态
  • TYPE即为图中第一行的8~11位
  • addr为中断处理函数地址
  • idt为中断描述符表,共256项,每项占8字节,在include/linux/head.h中定义
extern desc_table idt
typedef struct desc_struct {
	unsigned long a,b;
} desc_table[256];

即把system_call函数的地址存入和优先级、中断类型的信息存入中断描述符表中0x80号中断

--------------------------------------------------------------------------------------------------------------------------------------------

int 0x80  -->  kernel/system_call.s : 80 : _system_call  -->  include/linux/sys.h : 74 : sys_call_table[eax]()

------------------------------------------------------------------------------------------------------------------------------------

int 0x80在linux0.11中已经封装好系统调用接口,见include/unistd.h文件133~183行

共_syscall0、_syscall1、_syscall2、_syscall3四个宏函数,分别为无参~含有三个参数的系统调用

这里贴出无参的系统调用_syscall0代码

#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \                //将输出赋给变量__res,"=a"表示强制使用ax寄存器
	: "0" (__NR_##name)); \         //将__NR_name作为输入(系统调用号,在include/unistd.h定义),"0"表示使用与上一行一样的寄存器
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}

举个调用该宏函数的例子进行讲解

在init/main.c:23    调用该函数

static inline _syscall0(int,fork)

调用该函数的作用为定义了一个

静态(static) 内联(inline) 返回型为int(对应宏函数的type参数) 函数名为fork(对应宏函数name参数) 的无参函数:static inline int fork(void){}

该函数使用了内联汇编触发0x80号中断

--------------------------------------------------------------------------------------------------------------------------------------------

system_call

为C语言调用的汇编函数,在kernel/system_call.s第80行

主要作用为将进程从用户态切换到内核态,然后调用该文件94行的call  _sys_call_table(,%eax,4) 

这条指令的含义为跳转到sys_call_table + eax*4地址去执行。

  • sys_call_table为在include/linux/sys.h定义的C语言-函数指针数组,数组中存放了72个系统调用函数指针。
  • eax为中断号
  • *4是因为32位机每个指针占四个字节

即实际指令为跳转到系统调用函数指针数组中执行对应的系统调用。

以下为函数指针数组的初始化

typedef int (*fn_ptr)();                //在include/linux/sched.h 38行
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };

------------------------------------------------------------------------------------------------------------------------------------





欢迎大家指正、讨论

2011-05-05 22:09:00 yming0221 阅读数 16535
  • 韦东山嵌入式Linux第一期视频

    掌握写汇编代码的能力,可以分析任意裸板包括U-boot、内核里的相关汇编代码; 掌握常用的硬件部件的操作,比如GPIO,UART,I2C,LCD,触摸屏; 深入理解ARM体系统架构,可以写出具备中断功能的裸板程序,对程序现场的保存、恢复有所了解,这些原理适用于任何架构CPU,适用于内核及应用程序; 熟悉裸板程序的结构,给你一个Bootloader就能分析它的初始化部分、内存使用情况、链接地址、重定位、程序的相对跳转/绝对跳转等;掌握Bootloader启动内核的原理,能完全自己写出一个Bootloader。

    234477 人正在学习 去看看 韦东山

系统调用是一个软中断,中断号是0x80,它是上层应用程序与Linux系统内核进行交互通信的唯一接口。

这个中断的设置在kernel/sched.c中441行函数中

void sched_init(void)
{
	int i;
	struct desc_struct * p;

	if (sizeof(struct sigaction) != 16)
		panic("Struct sigaction MUST be 16 bytes");
	set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
	set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
	p = gdt+2+FIRST_TSS_ENTRY;
	for(i=1;i<NR_TASKS;i++) {
		task[i] = NULL;
		p->a=p->b=0;
		p++;
		p->a=p->b=0;
		p++;
	}
/* Clear NT, so that we won't have troubles with that later on */
	__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
	ltr(0);
	lldt(0);
	outb_p(0x36,0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */
	outb(LATCH >> 8 , 0x40);	/* MSB */
	set_intr_gate(0x20,&timer_interrupt);
	outb(inb_p(0x21)&~0x01,0x21);
	set_system_gate(0x80,&system_call);
}

最后一句就将0x80中断与system_call(系统调用)联系起来。

通过int 0x80,就可使用内核资源。不过,通常应用程序都是使用具有标准接口定义的C函数库间接的使用内核的系统调用,即应用程序调用C函数库中的函数,C函数库中再通过int 0x80进行系统调用。
    所以,系统调用过程是这样的:
    应用程序调用libc中的函数->libc中的函数引用系统调用宏->系统调用宏中使用int 0x80完成系统调用并返回

下面是sys_call_table的定义文件

位于./include/sys.h

 

其中sys_call_table的类型是fn_ptr类型,其中sys_call_table[0]元素为sys_setup,它的类型是fn_ptr类型,它实际上是函数sys_setup的

入口地址。

它的定义如下:

typedef int (*fn_ptr) (); // 定义函数指针类型。

下面的实例代码有助于理解函数指针:

 

 

system_call系统调用入口函数

./include/unistd.h文件中系统调用符号和调用号的对应定义

 

这是一系列宏,它们的定义在unistd.h中,基本形式为#define _NR_name value,name为系统函数名字,value是一个整数值,是name所对应的系统函数指针在sys_call_table中的偏移量。

 

系统调用宏也在本文件内定义,采用内联汇编,如下:

 

2018-04-19 19:42:02 hq815601489 阅读数 2067
  • 韦东山嵌入式Linux第一期视频

    掌握写汇编代码的能力,可以分析任意裸板包括U-boot、内核里的相关汇编代码; 掌握常用的硬件部件的操作,比如GPIO,UART,I2C,LCD,触摸屏; 深入理解ARM体系统架构,可以写出具备中断功能的裸板程序,对程序现场的保存、恢复有所了解,这些原理适用于任何架构CPU,适用于内核及应用程序; 熟悉裸板程序的结构,给你一个Bootloader就能分析它的初始化部分、内存使用情况、链接地址、重定位、程序的相对跳转/绝对跳转等;掌握Bootloader启动内核的原理,能完全自己写出一个Bootloader。

    234477 人正在学习 去看看 韦东山

系统调用

系统调用的概念

系统调用是计算机程序请求操作系统内核服务的方式,包括硬件相关的服务(例如访问硬盘驱动器)、创建和执行新的进程和进程调度等等。系统调用提供了进程和操作系统间的必要接口。

在大多数操作系统中,系统调用只能被用户空间进程使用。而在某些操作系统中,比如在OS/360及其后续的一些操作系统中,有特权的系统代码也会触发系统调用。

系统调用的分类

系统调用大体上可分为5类:

  • 进程控制
    • 加载
    • 执行
    • 结束,中止
    • 创建进程
    • 结束进程
    • 得到/设置进程属性
    • 等待(时间、时间、信号)
    • 内存的分配和去配
  • 文件管理
    • 文件的创建和删除
    • 打开和关闭
    • 读、写和重定位
    • 得到/设置文件属性
  • 设备管理
    • 设备的请求和释放
    • 读、写和重定位
    • 得到/设置设备属性
    • 设备的逻辑关联或去关联
  • 信息维护
    • 得到/设置时间或日期
    • 得到/设置系统数据
    • 得到/设置进程、文件或设备属性
  • 通信
    • 通信连接的创建和删除
    • 发送、接收信息
    • 转换状态信息
    • 远程设备的关联或去关联

Linux系统调用:使用 int 0x80

Linux提供了200多个系统调用,通过汇编指令 int 0x80 实现,用系统调用号来区分入口函数。

Linux实现系统调用的基本过程是:

  • 应用程序准备参数,发出调用请求;
  • C库封装函数引导。该函数在Linux提供的标准C库,即 glibc 中。对应的封装函数由下列汇编指令实现(以读函数调用为例):
; NASM
; read(int fd, void *buffer, size_t nbytes)
mov eax, 3          ; read系统调用号为3
mov ebx, fd
mov ecx, buffer
mov edx, nbytes
int 0x80            ; 触发系统调用
  • 执行系统调用。前两步在用户态工作,陷入后在内核态工作。系统调用处理程序根据系统调用号,按系统调用表中的偏移地址跳转,调用对应的内核函数;
  • 系统调用完成相应功能,将返回值存入 eax ,返回到中断处理函数;
  • 系统调用返回。内核函数处理完毕后,库函数读寄存器( eax )返回值,并返回给应用程序。恢复现场。

应用程序调用系统调用的过程是:

  • 把系统调用号存入 eax
  • 把函数参数存入其它通用寄存器(约定顺序为 ebxecxedxesiedi ,更多的参数(通常不会出现这种情况)使用堆栈传递,也可以通过寄存器存放指向参数在用户空间的地址指针来传递);
  • 触发 0x80 号中断( int 0x80 )。
  • 示例:
; NASM
; 向显示器输出hello, world
; write(int fd, const void *buffer, size_t nbytes)
; exit(int status)
        global  _start
        section .text
_start:
        mov     eax, 4              ; write系统调用号为4
        mov     ebx, 1              ; 文件描述符1:标准输出stdout
        mov     ecx, message        ; 要输出的信息
        mov     edx, message.len    ; 要输出的长度
        int     0x80

        mov     eax, 1              ; exit系统调用号为1
        mov     ebx, 0              ; 状态码0:正常退出
        int     0x80

        section .data
message:
        db      "hello, world", 10
.len    equ     $ - message

Linux系统调用实现机制

系统调用初始化

系统调用处理程序 system_call() 的入口地址放在系统的中断表述符表IDT(Interrupt Descriptor Table)中,Linux系统初始化时,由 trap_init() 将其填写完整,其设置系统调用处理程序的语句为:

set_system_gate(0x80, &system_call)

经过初始化以后,每当执行 int 0x80 指令时,产生一个异常使系统陷入内核空间并执行128号异常处理程序,即系统调用处理程序 system_call()

系统调用公共入口

system_call() 是所有系统调用的公共入口,其功能是保护现场,进行正确性检查,根据系统调用号跳转到具体的内核函数。内核函数执行完毕时需调用 ret_from_sys_call() ,这时完成返回用户空间前的最后检查,用 RESTORE_ALL 宏恢复现场并执行 iret 指令返回用户断点。

保护现场

  • 硬件(CPU)保护:ssespeflagscseip ,压入核心栈;
  • 软件(操作系统)保护
    • 使用 SAVE_ALL 宏将寄存器压入堆栈,加载内核的 dses ,往 edx 中放入 $(_KERNEL_DS) 以指明使用内核数据段,把内核数据段选择符装入 dses 。注意:该宏压入寄存器的顺序不是随意的,而是和系统调用的参数传递密切相关;
    • esdseaxebpediesiedxecxebx ,压入核心栈。

系统调用处理时的核心栈内容:

硬件完成
ss
esp
eflags
cs
eip
软件完成
es
ds
eax
ebp
edi
esi
edx
ecx
ebx

返回值传递

当内核函数返回到 system_call() 时, eax 中存放着内核函数的返回值。要将这个返回值传递给应用程序,内核先将 eax 放入原先 SAVE_ALL 宏保存 eax 的位置,这样当 system_call() 调用 RESTORE_ALL 恢复寄存器时, eax 便被恢复成系统调用的返回值,完成了返回值从内核空间到用户空间的传递。

系统调用号和系统调用表

系统调用的数量由 NR_syscalls 宏给定,每个系统调用所对应的编号已预先在系统文件中定义,且都用一个宏表示,其定义有如下形式:

#define _NR_exit 1
#define _NR_fork 2
#define _NR_read 3
...

Linux的系统调用号和内核函数映射关系的系统调用表也被预先定义在系统文件中,具有如下形式:

.data
ENTRY(sys_call_table)
    .long SYMBOL_NAME(sys_ni_syscall)    /* 空项 */
    .long SYMBOL_NAME(sys_exit)
    .long SYMBOL_NAME(sys_fork)
    .long SYMBOL_NAME(sys_read)
...

内核函数入口地址为: eax * 4 + sys_call_table

参考

[1] 维基百科(英文) - 系统调用:Wikipedia - System call
[2] 维基百科(中文) - 系统调用:维基百科 - 系统调用
[3] 《操作系统教程(第五版)》(费翔林、骆斌编著,高等教育出版社):1.3.4 Linux系统调用及其实现机制

更多资料

[1] Linux系统调用表:Linux系统调用表
[2] NASM入门教程:NASM Tutorial
[3] NASM官方文档:NASM官方文档

2008-09-06 20:59:00 tq02h2a 阅读数 1199
  • 韦东山嵌入式Linux第一期视频

    掌握写汇编代码的能力,可以分析任意裸板包括U-boot、内核里的相关汇编代码; 掌握常用的硬件部件的操作,比如GPIO,UART,I2C,LCD,触摸屏; 深入理解ARM体系统架构,可以写出具备中断功能的裸板程序,对程序现场的保存、恢复有所了解,这些原理适用于任何架构CPU,适用于内核及应用程序; 熟悉裸板程序的结构,给你一个Bootloader就能分析它的初始化部分、内存使用情况、链接地址、重定位、程序的相对跳转/绝对跳转等;掌握Bootloader启动内核的原理,能完全自己写出一个Bootloader。

    234477 人正在学习 去看看 韦东山
系统调用:操作系统分为用户态和核心态,当用户调用用户态函数的时候,需要进行用户态到核心态的切换,CPU需要对现场进行保存。如果用户直接调用系统调用,就不需要进行二态切换,提高代码运行效率。并且编译链接后生成的可执行的ELF体积小,运行效率高。用户可以通过系统提供的C语言API进行系统调用,同时也可以通过0x80中断进行调用。下面介绍通过中断进行调用的方法。
参数传递:功能号和返回值通过%eax来传递,参数一般在5个以下的通过寄存器%ebx,%ecx.%edx,%esi,%edi来传递,如果参数个数大于5个,则要通过堆栈来传递,按照c语言的参数传递方式,最后一个参数最先进栈。同时,把%esp传递给%ebx
Linux系统调用个数319个。
例子:
.data
       filename:.fill 256
       filecontent:.string "Hello World!"
       len:.int 12
       fd:.int -1
.text
       .global _start
       _start:
              movl $3 , %eax #系统调用read,从标准输入读入文件名
              movl $0 , %ebx
              movl $filename , %ecx
              movl $1024 , %edx
              int $0x80
              movl $filename , %ebx #将文件名保存
              movl %eax , %ecx
              xorb %al , %al
              movl $filename , %ebx
              movb %al , -1(%ebx , %ecx)
              movl $5 , %eax #调用系统调用open创建文件
              movl $filename , %ebx
              xorl %ecx , %ecx
              orl $20 , %ecx
              orl $100 , %ecx
              orl $10 , %ecx
              movl $0x0080, %edx
              int $0x80
              movl $fd , %ebx #将文件描述符保存
              movl %eax , (%ebx)            
              movl $4 , %eax #调用系统调用write,向文件中写字符串
              movl $fd , %ebx
              movl (%ebx) , %ebx
              movl $filecontent , %ecx
              movl $len , %edx
              movl (%edx) , %edx
              int $0x80
              movl $6, %eax #调用系统调用close,关闭文件
              movl $fd , %ebx
              movl (%ebx) , %ebx
              int $0x80        
              movl $1 , %eax
              movl $0 , %ebx
              int $0x80
2019-07-20 12:19:35 zDavid_2018 阅读数 267
  • 韦东山嵌入式Linux第一期视频

    掌握写汇编代码的能力,可以分析任意裸板包括U-boot、内核里的相关汇编代码; 掌握常用的硬件部件的操作,比如GPIO,UART,I2C,LCD,触摸屏; 深入理解ARM体系统架构,可以写出具备中断功能的裸板程序,对程序现场的保存、恢复有所了解,这些原理适用于任何架构CPU,适用于内核及应用程序; 熟悉裸板程序的结构,给你一个Bootloader就能分析它的初始化部分、内存使用情况、链接地址、重定位、程序的相对跳转/绝对跳转等;掌握Bootloader启动内核的原理,能完全自己写出一个Bootloader。

    234477 人正在学习 去看看 韦东山

参考《程序员的自我修养》第十二章:系统调用与API(这章篇幅很短,20多页,1个小时便可以读完)

一、系统调用与库函数

1.系统调用函数是系统内核抛出来给用户空间调用的接口,系统调用由用户态调用,在内核态执行。系统调用的功能广泛,例如创建/退出进程和线程、进程内存管理、对系统资源访问如文件、网络、进程间通信、硬件设备等。

Linux上使用0x80号中断作为系统调用的入口;

Windows上采用0x2E作为系统调用的入口;

2.库函数:在函数库文件中实现,执行只需要在用户态执行就可以。

3.二者区别:

库函数在库文件中

系统调用函数:在系统内核实现

库函数有可能还需要转调系统调用函数,比如:fopen,printf等,也有可能不需要转调系统调用,比如strlen,strcpy等

二、Linux系统调用

1.概念

系统调用由0x80中断完成,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号;

EAX=1 表示退出进程(exit)

EAX=2 表示创建进程(fork)

EAX=3 表示读取文件或IO(read)

每个系统调用 对应一个 内核源代码中的一个函数(以sys_开头,比如exit调用对应内核的sys_exit函数,系统调用返回时EAX又作为调用结果的返回值)

(后面会有详细实例说明,但要弄懂整个过程,还需只要下面的几个概念)

2.系统调用弊端:

(1)使用不便:需要记很多函数细节,没有包装

(2)各操作系统之间不兼容

3.运行库的提出:

因为系统调用的一些弊端,所以:“解决计算机的问题通过增加中间层来实现”

 这里插入一个c语言运行库的概念:

c程序要想正常运行,要有一套代码集合来支撑,这套代码包括:入口函数及其所依赖的函数所构建的集合,这样的集合成为运行时库。C语言的运行库成为CRT(C RuningTime Library);CRT中包含c标准库,c标准库又包含了24个c头文件(stdio.h、string.h、math.h stdlib.h time.h assert.h ....等等)

三、系统调用原理

1.特权级别的概念:

特权级别:在现代操作系统中有两种特权级别,分别是用户模式(user mode)和内核模式(kernel mode),也成用户态和内核态。

特权模式存在意义:

特权模式的存在,使操作系统在不同模式下运行不同代码,限制代码的权力,提高安全性和稳定性。

例如普通应用程序在用户态的模式下就会有很多操作受限制比如访问硬件设备等。

2.中断:

中断:是一个软件或者硬件发出的请求,要求CPU暂停当前的工作转手去处理更加重要的事情。

操作系统一般通过中断来从用户态切换到内核态。

3.中断的两种类型:

(1)硬件中断:电源掉电、键盘按下等等

(2)软件中断:通常是一条指令(在i386下是int),带有参数记录中断号,使用该指令用户可以手动触发某个中断并执行其中的中断处理程序。(例如在i386下,int 0x80这条指令,会调用第0x80号中断处理程序)

 4.中断的两个属性

(1)中断号:例如0x80

(2)中断处理程序

在内核中,有一个中断向量表的数组:第n项包含n号中断的中断处理程序指针

 因为中断号有限,操作系统不会用一个中断号对应一个系统调用,所以在Windows下,绝大数系统调用有0x2E触发;

在Linux下使用0x80触发所有系统调用。

那么系统如何知道这个0x80中断触发的哪一个系统调用呢???(如上图,eax寄存器啊!再根据系统调用表)

当然知道,因为每一个系统调用都有一个系统调用号(系统调用号在系统调用表中存放);例如Linux中fork的系统调用号是2,在执行0x80前2会存放在EAX寄存器中,对应的中断代码取得这个2,然后执行正确的函数(sys_fork),并且执行后的返回值也有EAX寄存器带回。

 (图片截取《程序员的自我修养》390页)

书中太剖析的触发0x80中断详细过程:(1)触发中断(2)切换堆栈(3)中断处理程序

......

......

......

四、Windows API

1.API:application programming interface 应用程序编程接口。API是对一系列事物的总称。在Windows下提到API时,一般就是指Windows提供给应用程序的接口,相当于Linux下的系统调用。

Linux下,系统调用是应用程序的最底层。Windows下,最低层的接口是Windows API。

如图一个fwirte的调用路径

从图中也可以看出,在触发中断时,Linux和Windows的区别,Linux下0x80中断在运行库中的系统调用之后触发,Windows多了一层API的封装,在API层中触发0x2E中断。

并由此可见,CRT(c运行时库,是在Windows API的上层)。

没有更多推荐了,返回首页