Linux0.11系统调用之execve流程解析

article/2025/8/12 15:21:47

Linux0.11系统调用之execve流程解析

  • 前言
  • execve功能介绍
  • execve本质
  • execve系统调用流程
  • 总结

前言

本文是基于Linux0.11源码来叙述该功能,源码可以在oldlinux.org上自行获取。

execve功能介绍

execve是用于运行用户程序(a.out)或shell脚本的函数,是linux编程中常用的一个系统调用类函数。在linux命令行下运行用户程序本质其实就是执行execve系统调用。

execve本质

在execve.c文件中execve被这样定义_syscall3(int,execve,const char *,file,char **,argv,char **,envp),其中_syscall3()是一个宏,将其展开后如下:

int execve(const char * file,char ** argv,char ** envp) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_execve),"b" ((long)(file)),"c" ((long)(argv)),"d" ((long)(envp))); \
if (__res>=0) \return (int) __res; \
errno=-__res; \
return -1; \
}

可以看到execve本质是系统调用int 0x80(类似于软中断的触发),系统调用号为__NR_execve赋值在eax当中,传入的参数分别为file、argv、envp由ebx、ecx、edx寄存器分别传入。
注:__NR_execve在unistd.h中定义,值为11,是sys_call_table的索引值(用于找到该表中对应的系统调用函数sys_execve)

execve系统调用流程

执行int 0x80后,CPU会跳转到_system_call执行,_system_call如下:

_system_call:cmpl $nr_system_calls-1,%eax #将系统调用号与系统调用max值对比ja bad_sys_call #如果超出范围,则跳转到bad_sys_call,是一个错误的系统调用号push %ds #用户数据端ds入栈,保护现场push %es #用户数据端ds入栈,保护现场push %fs #用户数据端ds入栈,保护现场pushl %edxpushl %ecx		pushl %ebx		# 将edx(file)、ecx(argv)、ebx(envp)入栈,作为C语言调用参数movl $0x10,%edx		# ds、es指向内核数据段mov %dx,%dsmov %dx,%esmovl $0x17,%edx		# fs指向用户数据段(是内核与用户沟通的桥梁)mov %dx,%fscall _sys_call_table(,%eax,4) #调用sys_call_table表中第__NR_execve项函数,即sys_execve....

_system_call检查了系统调用号的正确性后保护了用户数据段的现场,根据eax即__NR_execve调用sys_call_table系统调用表中的sys_execve函数,sys_execve也是一个汇编函数如下:

_sys_execve:lea EIP(%esp),%eax #取堆栈中存放系统调用的返回地址的地址pushl %eax #将该地址入栈call _do_execve #调用do_execve函数addl $4,%esp #丢弃该地址ret

这边做了一个很重要的操作就是将堆栈中存放系统调用的返回地址的地址入栈(见下图中的PTR指针),注意这里是堆栈的地址!而非系统调用的返回地址(int 0x80 的下一句语句的地址)!!到这里,我们先观察一下当前的内核堆栈如下:
exec_stack

黄色部分:系统调用时CPU自动推入保护的参数,因为要陷入内核态,所以CS与SS、ESP需要被保存起来并置换成内核代码段、内核堆栈段、内核堆栈指针,保存EFLAGS现场,保存中断返回地址(系统调用返回地址)
蓝色部分:自_system_call起始代码推入堆栈的数据,其中倒数5个参数是接下来do_execve会调用的参数。

了解清楚堆栈后,接下来执行C函数do_execve的调用,do_excve函数如下(本文暂时先跳过shell的执行部分,先看看可执行文件如何被执行):

int do_execve(unsigned long * eip,long tmp,char * filename,char ** argv, char ** envp)//input : _system_call back addr, 
{struct m_inode * inode;struct buffer_head * bh;struct exec ex;unsigned long page[MAX_ARG_PAGES];//存放物理页地址,总共可以使用32个物理页用于存放参数int i,argc,envc;int e_uid, e_gid;int retval;int sh_bang = 0;//涉及shell,暂时先不看unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;//指向[32个物理页的末端-4]地址if ((0xffff & eip[1]) != 0x000f)//eip指针指向堆栈图中黄色区域的EIP,那么eip[1]就是用户代码段CSpanic("execve called from supervisor mode");//如果CS指向内核数据段则宕机,不允许内核使用for (i=0 ; i<MAX_ARG_PAGES ; i++)	//将32个存放物理页地址的数组清零page[i]=0;if (!(inode=namei(filename))) //获取filename可执行文件的inodereturn -ENOENT;//获取不到则返回错误argc = count(argv);//计算arg参数个数envc = count(envp);//计算env参数个数restart_interp:if (!S_ISREG(inode->i_mode)) {	//查看可执行文件的inode是否是常规文件,不是则发生错误retval = -EACCES;goto exec_error2;}i = inode->i_mode;e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;//如果S_ISUID置位,那么有效的UID就是inode的uid,否则沿用当前进程的uide_gid = (i & S_ISGID) ? inode->i_gid : current->egid;//如果S_ISGID置位,那么有效的GID就是inode的gid,否则沿用当前进程的gidif (current->euid == inode->i_uid)//如果进程id与文件的user id一致,那么使用user idi >>= 6;else if (current->egid == inode->i_gid)//如果进程id与文件的group id一致,那么使用group idi >>= 3;if (!(i & 1) &&!((inode->i_mode & 0111) && suser())) {//判断是否拥有执行权限或是否超级用户retval = -ENOEXEC;//没有执行权限,输入返回值ENOEXECgoto exec_error2;//跳转到错误处理位置}if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {//根据inode读取文件的第一个block,block号存放在inode->i_zone[0]内,读哪个设备由inode->i_dev来定retval = -EACCES;//读不到数据则返回错误EACCESgoto exec_error2;//跳转到错误处理位置}ex = *((struct exec *) bh->b_data);	//读取exec头部if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {//如果文件开头是#!开头,则可能是shell脚本......//这里是shell的处理,本篇先不做赘述}brelse(bh);//释放buffer head,因为已经得到了exec头部(第一个block中只有exec头部信息有效),所以释放if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {//检查exec头魔数是否为ZMAGIC,a_trsize与a_drsize是否为零,代码数据长度不得大于48M,inode的i_size不可小于代码段大小+数据段大小+符号表大小+exec头部占用大小retval = -ENOEXEC;//否则返回错误值ENOEXECgoto exec_error2;}if (N_TXTOFF(ex) != BLOCK_SIZE) {//exec头部必须占有BLOCK_SIZE大小printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);retval = -ENOEXEC;goto exec_error2;}if (!sh_bang) {//不是shellp = copy_strings(envc,envp,page,p,0);//拷贝参数到page当中(里面会分配物理页)p = copy_strings(argc,argv,page,p,0);//拷贝环境变量到page当中(里面会分配物理页)if (!p) {retval = -ENOMEM;goto exec_error2;}}if (current->executable)//如果当前进程是可执行文件iput(current->executable);//那么释放当前的可执行文件inode节点current->executable = inode;//当前可执行文件inode赋值为最新for (i=0 ; i<32 ; i++)//信号处理函数全部清空current->sigaction[i].sa_handler = NULL;for (i=0 ; i<NR_OPEN ; i++)//关闭exec时需要关闭的句柄if ((current->close_on_exec>>i)&1)sys_close(i);current->close_on_exec = 0;free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));//清空当前进程的页表映射free_page_tables(get_base(current->ldt[2]),get_limit(0x17));//清空当前进程的页表映射if (last_task_used_math == current)last_task_used_math = NULL;current->used_math = 0;p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;//改变ldtp = (unsigned long) create_tables((char *)p,argc,envc);//制作参数表(类似指针数组)current->brk = ex.a_bss +(current->end_data = ex.a_data +(current->end_code = ex.a_text));//写入代码结束位置、数据结束位置、bss结束位置current->start_stack = p & 0xfffff000;//记录堆栈指针所在页current->euid = e_uid;current->egid = e_gid;i = ex.a_text+ex.a_data;while (i&0xfff)//如果数据末端不是页对齐(4KB对齐),那么将数据末端到页末端的数据清零put_fs_byte(0,(char *) (i++));//我感觉这里会触发缺页异常,因为page_table被free了,后来发现 a_text+a_data几乎就4kb对齐了,好像进不来这eip[0] = ex.a_entry;		//上图堆栈图中黄色部分EIP系统调用返回地址被替换成用户程序入口地址eip[3] = p;			//上图堆栈图中黄色部分ESP用户堆栈指针被替换成preturn 0;//返回0
exec_error2:iput(inode);
exec_error1:for (i=0 ; i<MAX_ARG_PAGES ; i++)free_page(page[i]);return(retval);
}

上述代码如果看不懂,继续往下看分析,接下来截取关键逻辑进行解析:

  1. 首先filename是可执行文件,我们必须读取可执行文件(a.out)在磁盘中的数据才可以运行,那么我们要找到其在硬盘中的位置,需要依托inode,因此我们使用
    inode=namei(filename)读取到filename的inode节点(这是根据根目录inode或者当前目录inode一路索引找到)。

  2. 获取到了可执行文件的inode节点,使用bh = bread(inode->i_dev,inode->i_zone[0])块设备读取函数读取其第一块数据,其中i_dev是设备号(从哪个块设备读取),i_zone[0]是逻辑块号(读取块设备中的哪一个部分)。读到了可执行文件的首个block(1KB)后,ex = *((struct exec *) bh->b_data)将exec头部读取出来,第一个block中只有exec头部的数据是有效的,用于记录可执行文件的一些信息,exec头部结构体struct exec如下所示:

    struct exec {unsigned long a_magic;	/* Use macros N_MAGIC, etc for access */unsigned a_text;		/* length of text, in bytes */unsigned a_data;		/* length of data, in bytes */unsigned a_bss;		/* length of uninitialized data area for file, in bytes */unsigned a_syms;		/* length of symbol table data in file, in bytes */unsigned a_entry;		/* start address */unsigned a_trsize;		/* length of relocation info for text, in bytes */unsigned a_drsize;		/* length of relocation info for data, in bytes */
    };
    

    由图可见,exec头部包含了二进制文件的代码长度、数据长度、bss段长度、符号表长度、起始地址、数据代码重定位信息(这个没用到)。

  3. 有了可执行文件的基本数据,接下来要准备运行的环境,需要修改ldt局部描述符表,否则代码数据可能会无法被访问到,因为有限长,并且需要先把当前进程的页表映射全部切断。使用free_page_tables(get_base(current->ldt[1]),get_limit(0x0f))free_page_tables(get_base(current->ldt[2]),get_limit(0x17))分别切断代码段和数据段的页表映射。使用change_ldt(ex.a_text,page)函数修改ldt局部描述符表,将代码段起始与数据段起始一致均从0开始,代码段限长修改为ex.a_text的长度,数据段限长修改为0x4000000,可以访问到整个进程内的所有数据,因为一个进程的空间为64M,这里限长正好就是64M。代码片段如下:

    static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
    {unsigned long code_limit,data_limit,code_base,data_base;int i;code_limit = text_size+PAGE_SIZE -1;//计算代码段所占页面code_limit &= 0xFFFFF000;//页对齐data_limit = 0x4000000;//64Mcode_base = get_base(current->ldt[1]);//获取代码段起始data_base = code_base;//数据段与代码段起始一致,这里均为0set_base(current->ldt[1],code_base);//设置代码段起始set_limit(current->ldt[1],code_limit);//设置代码段限长set_base(current->ldt[2],data_base);//设置数据段起始set_limit(current->ldt[2],data_limit);//设置数据段限长
    /* make sure fs points to the NEW data segment */__asm__("pushl $0x17\n\tpop %%fs"::);//fs赋值0x17代表指向用户数据段data_base += data_limit;//指向末端for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {//这里是填充参数页,将存有参数的物理页填入页表形成映射data_base -= PAGE_SIZE;if (page[i])put_page(page[i],data_base);}return data_limit;
    }
    
  4. 环境的准备了解到了,那么得考虑一下参数如何传入,argv参数与env环境变量如何传入到用户程序当中。系统预留了32个页(4KB*32)的空间用于存放参数及环境变量,使用p作为空间的索引(类似堆栈指针,向下生长),p初始指向32页空间的最后4bytes位置(unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4),copy_strings(envc,envp,page,p,0)copy_strings(argc,argv,page,p,0)将参数及环境变量如同堆栈一般,从上往下压入32页空间,如果未分配则分配物理页,代码如下:

    static unsigned long copy_strings(int argc,char ** argv,unsigned long *page,unsigned long p, int from_kmem)
    {char *tmp, *pag;int len, offset = 0;unsigned long old_fs, new_fs;if (!p)return 0;	/* bullet-proofing */while (argc-- > 0) {//argc是参数的个数if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))//指向第argc个参数,如果为空,宕机panic("argc is wrong");len=0;		/* remember zero-padding */do {len++;} while (get_fs_byte(tmp++));//计算参数长度,即字符串长度,以‘\0’结尾if (p-len < 0) {	 //确认32页空间(32*4KB=128KB)是否无法容纳新的参数return 0;}while (len) {//循环参数长度(一个一个字节存入)--p; --tmp; --len;//p指向新的一个字节空间,tmp指向待复制的参数尾端,len代表剩余长度if (--offset < 0) {//页内偏移小于0offset = p % PAGE_SIZE;//重置页内偏移if (!(pag = (char *) page[p/PAGE_SIZE]) &&!(pag = (char *) page[p/PAGE_SIZE] =(unsigned long *) get_free_page())) //如果该页不存在则分配return 0;}*(pag + offset) = get_fs_byte(tmp);//从用户空间将一字节复制到物理页内}}return p;
    }
    

    后文将参数与环境变量统称为参数。以上代码经过轻微删减,因为from_kmem为0,即我们要复制的参数都是从用户空间而来,通过代码可以感受到,参数变量从用户空间被复制,并被存入32页的参数空间p指向的位置(从上往下),参数变量的实质就是字符串,复制时每个字符串的尾末’\0’也会被复制,用以分割每个参数。执行完复制后,参数页的逻辑空间如下图所示(假设有2个参数2个环境变量):
    copy1

  5. 参数占用多少物理页,就进行多少页的映射,页表映射的操作在change_ldt函数的后半部分(前半部代码上面解释过,此处省略掉),预留的32页参数页从最后一页开始,将参数占用的页put_page(),放到64M(Linux0.11中每个进程的逻辑空间为64M)末端,建立映射,代码如下:

     static unsigned long change_ldt(unsigned long text_size,unsigned long * page){unsigned long code_limit,data_limit,code_base,data_base;int i;.....//省略了其他代码data_base += data_limit;//指向末端for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {//这里是填充参数页,将存有参数的物理页填入页表形成映射,用了多少页就映射多少页data_base -= PAGE_SIZE;//指向还未使用的最后一页(逻辑页)if (page[i])//物理页存在(则表示该页负载了参数或环境变量)put_page(page[i],data_base);//将物理页与逻辑页形成映射}return data_limit;}
    
  6. 制作环境变量、参数指针表,由上可见参数、环境变量仅仅是被拷贝过去并且实现页表映射,但参数、环境变量使用起来不是很方便且参数与环境变量没有明确的分界(就算根据p读出字符串,但也不知道该字符串是环境变量还是参数),因此使用create_tables((char *)p,argc,envc)进行指针表的制作。

    static unsigned long * create_tables(char * p,int argc,int envc)
    {unsigned long *argv,*envp;unsigned long * sp;sp = (unsigned long *) (0xfffffffc & (unsigned long) p);//4byte对齐sp -= envc+1;//留出 环境变量数+1 个指针空间envp = sp;//记录环境变量指针空间首地址sp -= argc+1;//留出 参数个数+1 个指针空间argv = sp;//记录参数指针空间首地址put_fs_long((unsigned long)envp,--sp);//存入环境变量指针空间首地址put_fs_long((unsigned long)argv,--sp);//存入参数指针空间首地址put_fs_long((unsigned long)argc,--sp);//存入参数的个数while (argc-->0) {put_fs_long((unsigned long) p,argv++);//将各个参数的首地址依次放入参数指针空间while (get_fs_byte(p++)) ;//读取下一个参数的首地址(因为是字符串,所有以0作为分隔)}put_fs_long(0,argv);//指针空间必须以NULL结尾while (envc-->0) {//同理放入环境变量put_fs_long((unsigned long) p,envp++);while (get_fs_byte(p++)) /* nothing */ ;}put_fs_long(0,envp);//指针空间必须以NULL结尾return sp;
    }
    

    指针表制作完毕后,32页的参数空间如下所示(假设只有2个参数、2个环境变量):
    在这里插入图片描述
    如上图所示,可以看到形成了2个指针数组(指针表),如果写成C语言则是,分别是unsigned int *arg0_ptr_ptr[] = {arg0_ptr, arg1_ptr, NULL}unsigned int *env0_ptr_ptr[] = {env0_ptr, env1_ptr, NULL},那么我们可以想象到可执行文件a.out中的main()函数传入的参数argc,argv就是图中的int argcint **arg0_ptr_ptr

  7. 修改跳转地址及用户堆栈,使系统调用返回地址改为可执行文件的进入地址(在Linux0.11中为0),修改eip[0] = ex.a_entry;eip[3] = p;就这么简单,把系统调用(中断)返回地址修改成了用户程序的入口地址,用户堆栈修改成了p。到了这里,堆栈的空间如下图所示(eip就是下图中的PTR,因此不难推算出eip[0]和eip[3]修改的是堆栈中的哪几个参数):
    exec_stack_after
    由图可见,红框内是被修改的值,可以参照上面未修改的堆栈图作对比,那么当系统调用返回时,CPU指令指针的指向是ex.a_entry,堆栈是p

  8. 至此,execve的系统调用基本完成,肯定有人有疑惑,当前进程的页表也仅仅只映射了参数区块的页表,代码没拷贝没映射,系统调用(中断)返回时不会产生异常么?是的,确实会产生异常,当执行到了可执行文件a.out未被拷贝入物理内存的指令或数据时,会产生缺页异常(异常发生的原因是页表present值为0,未映射,不存在),缺页异常会将a.out的代码拷贝入新分配的物理内存页而后进行逻辑地址与物理地址的映射,即填充页表,异常处理结束后返回发生异常的地址重新执行代码。因此,所谓的用户程序(包括你平时玩的游戏)在运行时,并不一定整个游戏的代码就在你的运行内存当中,事实是访问到了才被拷贝入运行内存(按页拷贝,4KB)。
    参考文章: Linux0.11系统异常之页异常

  9. 那么看看当前进程的64M逻辑地址分布,如下图所示:
    应用程序逻辑空间
    由图可以很明晰得看到,参数、环境变量被置于64M顶端,p紧贴其后,代码段从nr*-0x04000000开始(nr是任务结构体的索引值),当然访问代码段的时候从0地址开始即可,因为CPU在保护模式下的寻址是会加上段基址(段基址是存储在ldt当中的,图中段基址nr*0x04000000),所以编译应用程序的时候都是从0地址开始,包括ex.a_entry值也是0。上图中的蓝色区域目前是不存在的,都是应用程序执行后才可能出现的,堆栈与堆相向而行,但空间足够大,基本不会碰着面。

总结

evecve系统调用将当前进程重新划分,为可执行文件划出合理的空间,并将参数置于当前进程64M末端,为应用程序的执行做好了准备。


http://chatgpt.dhexx.cn/article/QWbkQrUB.shtml

相关文章

5.execve()到底干了啥?

导语 很多童鞋有分析阅读Linux源代码的强烈愿望&#xff0c;可是Linux内核代码量庞大&#xff0c;大部分人不知道如何下手&#xff0c;以下是我分析Linux源代码的一些经验&#xff0c;仅供参考&#xff0c;有不实之处请大神指正&#xff01; 1.要想阅读内核首先要进入内核&…

Linux系统编程(再论execve)

文章目录 前言一、execve的第三个参数二、进程空间三、命令行参数规范四、optstring规则的扩展定义总结 前言 本篇文章我们继续来研究一下execve这个系统调用&#xff0c;上篇文章已经讲解了前两个参数的意义了&#xff0c;那么这篇文章就来讲解一下第三个参数的具体含义。 一…

Linux0.11 execve函数(六)

系列文章目录 Linux 0.11启动过程分析&#xff08;一&#xff09; Linux 0.11 fork 函数&#xff08;二&#xff09; Linux0.11 缺页处理&#xff08;三&#xff09; Linux0.11 根文件系统挂载&#xff08;四&#xff09; Linux0.11 文件打开open函数&#xff08;五&#xff09…

C语言 execve()函数使用方法

1.君の名は execve() – 叫做执行程序函数 就像Python中的os.system(cmd)这个函数&#xff0c;我们可以用这个函数来执行我们的shell脚本&#xff0c;单独的shell命令&#xff0c;或者是调用其他的程序&#xff0c;我们的execve()这个函数就和Python中的os.system函数类似&am…

SPSS-因子分析

因子分析 有可能用较少的综合指标分析存在于各变量中的各类信息&#xff0c;而各综合指标之间彼此是不相关的&#xff0c;代表各类信息的综合指标称为因子。定义&#xff1a;因子分析就是用少数几个因子来描述许多指标或因素之间的联系&#xff0c;以较少几个因子反映原资料的 …

spss进行主成分分析

什么是主成分分析 简而概之, 就是一组数据受太多因素影响, 选出几个能代表他们的因素,并进行线性组合得到一组比原维度小的因素组合, 作为新的因素集 用spss操作 随手拿出一组数据 1.数据统一标准化 因为我们得到的原始数据大小,类型不一, 一起分析会不准确, 所以将数据全部…

实用干货!因子分析超全步骤总结

因子分析是统计数据分析方法之一&#xff0c;因子分析包括探索性因子分析和验证性因子分析。本文主要讨论探索性因子分析。 一、研究背景 关于工作满意度有14个问题&#xff0c;调研得到215份问卷结果。希望通过因子分析&#xff0c;用少量因子反映14个题目的信息&#xff0c;…

NLP | 朴素贝叶斯法的学习与分类

朴素贝叶斯法的学习与分类 《统计学习方法》李航第四章 1、概述 书上对朴素贝叶斯的描述如下&#xff1a; 朴素贝叶斯法时基于贝叶斯定理与特征条件独立假设的分类方法。对于给定的训练数据集&#xff0c;首先基于特征条件独立假设学习输入/输出的联合概率分布&#xff1b;然…

标定相机参数-张正友方法

一、实验原理 1.计算外参 设三维世界坐标的点为M=[X,Y,Z,1]T,二维相机平面像素坐标为m=[u,v,1]T,所以标定用的棋盘格平面到图像平面的单应性关系为: sm=A[R,t]M 其中 不妨设棋盘格位于Z = 0,定义旋转矩阵R的第i列为 ri, 则有: 令H=[h1 h2 h3]=λA[r1 r2 t] 于是空间到图…

《统计学习方法》——朴素贝叶斯法

引言 朴素贝叶斯法(Naive Bayes)是基于贝叶斯定理与特征条件独立假设的分类方法。朴素贝叶斯法实现简单&#xff0c;学习与预测的效率都很高&#xff0c;是一种常用的方法。 这一章需要大量的概率论知识&#xff0c;忘记了的同学建议先参阅人工智能数学基础之概率论。 朴素贝…

数据挖掘十大算法之 naïve Bayes

朴素贝叶斯法是基于贝叶斯定理和特征条件独立假设的分类方法。朴素贝叶斯法实现简单&#xff0c;学习与预测的效率都很高&#xff0c;被广泛应用于文本分类、垃圾邮件过滤、自然语言处理等场景。下面我们来介绍贝叶斯定理&#xff0c;在介绍贝叶斯定理之前&#xff0c;先介绍下…

专题:深度神经网络基本问题的原理详细分析和推导

文章目录 **写在最前面****1 神经网络算法的直观了解****1.1 神经网络过程描述**&#xff1a;**1.2 神经网络相关的几个问题****1.2.1 表征假设和激活函数** **1.2.2 结构设计(Architecture Design)****1.2.3 代价函数(Cost Function)和优化目标(Optimization objective)****1.…

第四章 朴素贝叶斯法

文章目录 朴素贝叶斯法的学习与分类基本方法数据定义学习联合概率分布如何求出条件概率分布&#xff1f;如何分类&#xff1f; 后验概率最大化的含义 朴素贝叶斯的参数估计法极大似然估计学习分类算法贝叶斯估计 朴素贝叶斯法&#xff08;与贝叶斯估计是不同的概念&#xff09;…

GAN生成对抗式神经网络数学推导

由上面一篇文章我们已经知道了&#xff0c;如果我们从真实数据分布里面取n个样本&#xff0c;根据给定样本我们可以列出其出现概率的表达式&#xff0c;那么生成这N个样本数据的似然(likelihood)就是 l ( θ ) ∏ i 1 N p ( x i ∣ θ ) l ( \theta ) \prod _ { i 1 } ^ { …

《统计学习方法》学习笔记(三)之 朴素贝叶斯法

朴素贝叶斯法 总述 朴素贝叶斯法是基于贝叶斯定理与特征条件独立性假设的分类方法。对于给定的训练数据集&#xff0c;首先基于特征独立性假设学习输入/输出的联合概率分布&#xff1b;然后基于此模型&#xff0c;对给定的输入 x x x&#xff0c;利用贝叶斯定理求出后验概率最…

朴素贝叶斯(二)|极大似然估计+学习与分类算法+贝叶斯估计| 《统计学习方法》学习笔记(十六)

朴素贝叶斯法的参数估计 1. 极大似然估计 在朴素贝叶斯法中&#xff0c;学习意味着估计 P ( Y c k ) P(Yc_k) P(Yck​)和 P ( X ( j ) x ( j ) ∣ Y c k ) P(X^{(j)}x^{(j)}|Yc_k) P(X(j)x(j)∣Yck​)。可以应用极大似然估计法估计相应的概率。先验概率 P ( Y c k ) P(Yc…

一文看懂 “极大似然估计” 与 “最大后验估计” —— 最大后验估计篇

本文历次修订后全长 2万8000余字&#xff0c;受到 CSDN 博文字数限制&#xff0c;故切分两篇发布&#xff0c;所以现在是两文看懂了… 前篇介绍参数估计背景和极大似然估计&#xff1b;本篇介绍最大后验估计和两种方法对比请务必先看前文&#xff1a;一文看懂 “极大似然估计”…

【生成模型】极大似然估计,你必须掌握的概率模型

上一期为大家说明了什么是无监督生成模型。在无监督生成模型中&#xff0c;极大似然法一直扮演着非常核心的位置&#xff0c;我们必须对它有深刻的理解&#xff0c;本期小米粥将为大家讲一下极大似然法的那些事情。 作者&编辑 | 小米粥 1 一个小游戏&#xff1a;取球猜概率…

透彻理解机器学习中极大似然估计MLE的原理(附3D可视化代码)

文章目录 相关资料一、什么是概率&#xff0c;什么是似然二、极大似然估计 Maximum Likelihood Estimation (MLE) 的含义2.1 机器学习中的极大化似然函数2.2 极大似然估计和损失函数的关系VAE最大化似然函数推导出损失函数 三、代码可视化&#xff1a;极大似然估计3.1 似然函数…

C#RSA密码以及利用欧几里得算法实现两数互质的判断

最近做课程设计,想到以前看过RSA密码的相关内容&#xff0c;于是就想用刚学的C#做一个数字加密系统。RSA加密的流程如下&#xff1a; 来看一个“玩具式”的例子&#xff1a; (1)选取两个素数p2,q11,于是N22. (2)构造数,这是小于22且不含因数2和11的自然数的个数。 (3)选一个…