2018-11-16 09:14:34 weixin_43255133 阅读数 115
  • 自己动手从0到1写嵌入式操作系统

    这不是rtos源码分析的课程,而是为初级的同学设计,从基础原理讲师,一步步不断迭代设计rtos的课程! 用不到【2000行代码,汇编代码仅18行】(不含注释)实现一个精巧的可以运行在ARM Cortex-M内核芯片上的RTOS! 该RTOS功能与ucos类似,具体实现不同。学习之后,再去学习ucos之类的系统将没有什么问题。

    9215 人正在学习 去看看 李述铜

2.6 Linux 内核对内存的使用方法

在Linux 0.11 内核中,为了有效地使用机器中的物理内存,内存被划分成几个功能区域,见下图 2-9 所示。
在这里插入图片描述其中,Linux 内核程序占据在物理内存的开始部分,接下来是用于供硬盘或软盘等块设备使用的高速缓冲区部分。当一个进程需要读取块设备中的数据时,系统会首先将数据读到高速缓冲区中;当有数据需要写到块设备上去时,系统也是先将数据放到高速缓冲区中,然后由块设备驱动程序写到设备上。 后部分是供所有程序可以随时申请使用的主内存区部分。内核程序在使用主内存区时,也同样要首先向内核的内存管理模块提出申请,在申请成功后方能使用。对于含有 RAM 虚拟盘的系统,主内存区头 部还要划去一部分,供虚拟盘存放数据。
在 Linux 0.11 内核中,在进行地址映射时,我们需要首先分清 3 种地址以及它们之间的变换概念:a. 程序(进程)的逻辑地址;b. CPU 的线性地址;c. 实际物理内存地址。
逻辑地址(Logical Address)是指有程序产生的与段相关的偏移地址部分。在 Intel 保护模式下即是指程序执行代码段限长内的偏移地址(假定代码段、数据段完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对他来说是完全透明的,仅由系统编程人员涉及。
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G。
物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的。
在内存分段系统中,一个程序的逻辑地址是通过分段机制自动地映射(变换)到中间层的线性地址上。每次对内存的引用都是对内存段中内存的引用。当一个程序引用一个内存地址时,通过把相应的段 基址加到程序员看得见的逻辑地址上就形成了一个对应的线性地址。此时若没有启用分页机制,则该线性地址就被送到 CPU 的外部地址总线上,用于直接寻址对应的物理内存。
若采用了分页机制,则此时线性地址只是一个中间结果,还需要使用分页机制进行变换,再终映射到实际物理内存地址上。与分段机制类似,分页机制允许我们重新定向(变换)每次内存引用,以适应我们的特殊要求。使用分页机制普遍的场合是当系统内存实际上被分成很多凌乱的块时,它可以建立一个大而连续的内存空间的映象,好让程序不用操心和管理这些分散的内存块。分页机制增强了分段机制 的性能。页地址变换是建立在段变换基础之上的。任何分页机制的保护措施并不会取代段变换的保护措施而只是进行更进一步的检查操 作。
Intel CPU 使用段(Segment)的概念来对程序进行寻址。每个段定义了内存中的某个区域以及访问 的优先级等信息。而每个程序都可有若干个内存段组成。程序的逻辑地址(或称为虚拟地址)即是用于 寻址这些段和段中具体地址位置。在 Linux 0.11 中,程序逻辑地址到线性地址的变换过程使用了 CPU 的全局段描述符表GDT和局部段描述符表LDT。由 LDT 映射的地址空间称为全局地址空间,由LDT映射的地址空间则称为局部地址空间,而这两者构成了虚拟地址的空间。具体的使用方式见图 2-10 所示。
在这里插入图片描述

2.8 Linux 内核源代码的目录结构

当我们使用 tar 命令将 linux-0.11.tar.gz 解开时,内核源代码文件被放到了 linux/目录中。其中的目录结构见图 2-15 所示:
在这里插入图片描述

2.9 内核系统与用户程序的关系

在 Linux 系统中,内核为应用程序提供了两方面的接口。其一是系统调用接口(在第 5 章中说明), 也即中断调用 int 0x80;另一方面是通过内核库函数(在第 12 章中说明)与内核进行信息交流。内核库 函数是基本C函数库libc的组成部分。许多系统调用是作为基本 C 语言函数库的一部分实现的。系统调用主要是提供给系统软件直接使用或用于库函数的实现。而一般用户开发的程序则是通过调用象libc等库中的函数来访问内核资源。通过调用这些库中的程序,应用程序代码能够完成各种常用工作,例如,打开和关闭对文件或设备的访问、进行科学计算、出错处理以及访问组和用户标识号ID等系统信息。

2.10 linux/Makefile 文件

Makefile 文件相当于程序编译过程中的批处理文件。是工具程序 make 运行时的输入数据文件。只 要在含有 Makefile 的当前目录中键入 make 命令,它就会依据 Makefile 文件中的设置对源程序或目标代 码文件进行编译、连接或进行安装等活动。
make工具程序能自动地确定一个大程序系统中那些程序文件需要被重新编译,并发出命令对这些程 序文件进行编译。在使用make之前,需要编写 Makefile 信息文件,该文件描述了整个程序包中各程序之间的关系,并针对每个需要更新的文件给出具体的控制命令。通常,执行程序是根据其目标文件进行更新的,而这些目标文件则是由编译程序创建的。一旦编写好一个合适的 Makefile 文件,那么在你每次修改过程序系统中的某些源代码文件后,执行 make 命令就能进行所有必要的重新编译工作。
Makefile 文件的主要作用是指示make程序终使用独立编译连接成的 tools/目录中的 build 执 行程序将所有内核编译代码连接和合并成一个可运行的内核映像文件 image。具体是对 boot/中的bootsect.s、setup.s使用8086汇编器进行编译,分别生成各自的执行模块。再对源代码中的其它所有程序 使用 GNU 的编译器 gcc/gas进行编译,并连接成模块 system。再用 build工具将这三块组合成一个内核映象文件image. 基本编译连接/组合结构如图 2-20 所示。
在这里插入图片描述make 的执行过程分为两个不同的阶段。在第一个阶段,它读取所有的 makefile 文件以及包含的 makefile 文件等,记录所有的变量及其值、隐式的或显式的规则,并构造出所有目标对象及其先决条件的一幅全景图。在第二阶段期间,make 就使用这些内部结构来确定哪个目标对象需要被重建,并且使用 相应的规则来操作。
当 make 重新编译程序时,每个修改过的 C 代码文件必须被重新编译。如果一个头文件被修改过了, 那么为了确保正确,每一个包含该头文件的 C 代码程序都将被重新编译。每次编译操作都产生一个与源程序对应的目标文件(object file)。如果任何源代码文件被编译过了,那么所有的目标文件不管是 刚编译完的还是以前就编译好的必须连接在一起以生成新的可执行文件。
简单的 makefile 文件含有一些规则,这些规则具有如下的形式:
在这里插入图片描述其中’目标’对象通常是程序生成的一个文件的名称;例如是一个可执行文件或目标文件。目标也可以 是所要采取活动的名字,比如’清除’(‘clean’)。'先决条件’是一个或多个文件名,是用作产生目标的输入条 件。通常一个目标依赖几个文件。而’命令’是 make 需要执行的操作。一个规则可以有多个命令,每一个命令自成一行。

小结

本章概述了Linux 早期操作系统的内核模式和体系结构。给出了 Linux 0.11 内核源代码的目录结构形式,并详细地介绍了各个子目录中代码文件的基本功能和层次关系。后从 Linux 内核主目录下的 makefile 文件着手,开始对内核源代码进行注释。

2019-05-25 19:56:15 qq_36321889 阅读数 157
  • 自己动手从0到1写嵌入式操作系统

    这不是rtos源码分析的课程,而是为初级的同学设计,从基础原理讲师,一步步不断迭代设计rtos的课程! 用不到【2000行代码,汇编代码仅18行】(不含注释)实现一个精巧的可以运行在ARM Cortex-M内核芯片上的RTOS! 该RTOS功能与ucos类似,具体实现不同。学习之后,再去学习ucos之类的系统将没有什么问题。

    9215 人正在学习 去看看 李述铜

续上篇博客Linux内核完全注释:第三章 内核引导启动程序-setup.s讲解

功能描述

  • head.s编译后,被连接成system模块的最前面开始部分。
  • 这部分采用AT&T格式编写,赋值方向与之前相反
  • 首先加载各个段寄存器,重新设置中断描述符表,共256项,并使各个表项均指向一个只报错误的哑中断程序。
  • 重新设置全局描述符表,对比物理地址0与1M开始处的内容是否相同,如果相同那么没有开启A20地址线,进入死循环。
  • 测试PC机是否含有数据协处理器芯片,并在控制寄存器CR0中设置相应的标志位
  • 设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处。
  • 紧随其后放置共可寻址16MB内存的4个页表,并分别设置它们的表项。
  • 最后利用返回指令将预先放置在堆栈中的/init/main.c程序的入口地址弹出,运行main()程序。

源码解析

  1. 程序开始的数据、符号声明部分:

    /*
     *  linux/boot/head.s
     *
     *  (C) 1991  Linus Torvalds
     */
    
    /*
     *  head.s contains the 32-bit startup code.
     *
     * NOTE!!! Startup happens at absolute address 0x00000000, which is also where
     * the page directory will exist. The startup code will be overwritten by
     * the page directory.
     */
    .text
    .globl idt,gdt,pg_dir,tmp_floppy_area
    pg_dir:
    .globl startup_32
    

    这里需要注意的是:pg_dir,这是页表目录地址,后面会用到。

  2. 程序开始部分,设置寄存器值。

    startup_32:
    	movl $0x10,%eax 	# 这里是段选择符,而不是地址,0001 0000,表示特权级0,全局描述符表,第2个表项
    	mov %ax,%ds			# 关于表项内容可以回去看setup.s程序中的定义,第2个表项刚好是数据段描述符表项
    	mov %ax,%es
    	mov %ax,%fs
    	mov %ax,%gs
    	lss stack_start,%esp 	# stack_start应是编译程序生成的存放堆栈信息的地方
    	call setup_idt		# 设置中断描述符表
    	call setup_gdt		# 设置全局描述符表
    	movl $0x10,%eax		# reload all the segment registers,重新加载所有的段寄存器
    	mov %ax,%ds			# after changing gdt. CS was already
    	mov %ax,%es			# reloaded in 'setup_gdt'
    	mov %ax,%fs
    	mov %ax,%gs
    	lss stack_start,%esp
    	xorl %eax,%eax
    

    这部分程序有疑问的可能就是0x10那里了,在代码后面我进行了详细的注解。

    讲解两个子程序:setup_idt, setup_gdt,笔者在读的时候遇到的难点都写在注释里了,直接看代码及注释即可。

    /*
     *  setup_idt
     *
     *  sets up a idt with 256 entries pointing to
     *  ignore_int, interrupt gates. It then loads
     *  idt. Everything that wants to install itself
     *  in the idt-table may do so themselves. Interrupts
     *  are enabled elsewhere, when we can be relatively
     *  sure everything is ok. This routine will be over-
     *  written by the page tables.
     */
    setup_idt:
    	lea ignore_int,%edx		# 理解这段代码需要看下中断描述符表项结构
    	movl $0x00080000,%eax	# 0X0008赋值给eax的高16位,表示是cs段寄存器
    	movw %dx,%ax		/* selector = 0x0008 = cs */
    	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present ,设置一些固定的位*/ 
    							# 现在eax中包含中断描述符表项的低32位,edx包含高32位
    	lea idt,%edi			# 加载idt的地址
    	mov $256,%ecx			# 循环256次,一共设置256个中断描述符表项
    rp_sidt:
    	movl %eax,(%edi)
    	movl %edx,4(%edi)
    	addl $8,%edi
    	dec %ecx
    	jne rp_sidt
    	lidt idt_descr			# 加载中断描述符表寄存器
    	ret
    
    /*
     *  setup_gdt
     *
     *  This routines sets up a new gdt and loads it.
     *  Only two entries are currently built, the same
     *  ones that were built in init.s. The routine
     *  is VERY complicated at two whole lines, so this
     *  rather long comment is certainly needed :-).
     *  This routine will beoverwritten by the page tables.
     */
    setup_gdt:
    	lgdt gdt_descr			# 从上面的注释看Linus这人还是挺幽默的。
    							# 这里全局描述符表已经写好,直接加载进寄存器就Ok了。
    	ret
    

    上面的程序又涉及到ignore_int,是一个哑巴中断,只打印一句话:

    	/* This is the default interrupt "handler" :-) */
    int_msg:
    	.asciz "Unknown interrupt\n\r"
    .align 2
    ignore_int:
    	pushl %eax
    	pushl %ecx
    	pushl %edx
    	push %ds			# 这里注意虽然ds这些段寄存器是16位的,但是压栈进入以后仍然以32位保存
    	push %es
    	push %fs
    	movl $0x10,%eax
    	mov %ax,%ds
    	mov %ax,%es
    	mov %ax,%fs
    	pushl $int_msg		# printk函数的参数
    	call printk			# 调用printk函数,打印Unknown iterrupt
    	popl %eax
    	pop %fs
    	pop %es
    	pop %ds
    	popl %edx
    	popl %ecx
    	popl %eax
    	iret
    
  3. 判断A20地址线是否打开,这里很巧妙。关于A20地址线上篇博客提到过,可以回去看,也可以自己查。

    1:	incl %eax			# check that A20 really IS enabled
    	movl %eax,0x000000	# loop forever if it isn't, 向0x000000中存入eax,并判断0x100000中的内容是否与之相同
    	cmpl %eax,0x100000	# 如果相同,那么没有开启A20地址线,无限循环,相当于死在这里了,需要重启吧
    	je 1b
    
  4. 协处理器相关内容(协处理器是专门用来处理特殊任务的,是专用芯片,而CPU是通用芯片),这部分涉及协处理器指令,笔者并没有深究。其基本思想是:使用一条协处理器指令,如果得到了预期的结果,说明存在协处理器,否则不存在。

    /*
     * NOTE! 486 should set bit 16, to check for write-protect in supervisor
     * mode. Then it would be unnecessary with the "verify_area()"-calls.
     * 486 users probably want to set the NE (#5) bit also, so as to use
     * int 16 for math errors.
     */
    	movl %cr0,%eax		# check math chip
    	andl $0x80000011,%eax	# Save PG,PE,ET
    /* "orl $0x10020,%eax" here for 486 might be good */
    	orl $2,%eax		# set MP
    	movl %eax,%cr0
    	call check_x87
    	jmp after_page_tables
    
    /*
     * We depend on ET to be correct. This checks for 287/387.
     * 这里的功能是:检查有没有287/387协处理器,并设置CR0标志位
     */
    check_x87:
    	fninit
    	fstsw %ax
    	cmpb $0,%al
    	je 1f			/* no coprocessor: have to set bits */
    	movl %cr0,%eax
    	xorl $6,%eax		/* reset MP, set EM */
    	movl %eax,%cr0
    	ret
    .align 2
    1:	.byte 0xDB,0xE4		/* fsetpm for 287, ignored by 387 */
    	ret
    
  5. 处理完协处理器后,跳转到after_page_table子程序:

    	after_page_tables:
    		pushl $0			# These are the parameters to main :-)
    							# 这些是main函数的参数
    		pushl $0
    		pushl $0
    		pushl $L6			# return address for main, if it decides to.
    		pushl $main			# 这里的main是编译程序对main的内部表示方法
    		jmp setup_paging	# 跳转,在setup_paging执行完毕后调用ret指令,会取出main的地址,即跳转到main继续执行
    	L6:
    		jmp L6				# main should never return here, but # 如果main函数返回了,那么陷入无限循环,即死机
    							# just in case, we know what happens.
    
  • 这里是跳转到main()函数的关键代码,其实是一种hack行为,不是多么正规,但也算是汇编语言的技巧吧。

  • 先压栈一个地址,然后调用jmp跳转到setup_paging(不使用call是因为,call会将返回地址自动压栈,而jmp什么都不会做)。

  • setup_paging执行完毕后调用ret指令,从栈中取返回地址,然后返回。

  • 而这里的返回地址刚好是之前入栈的main()函数入口地址,所以执行完这段程序就去执行main了。

    下面解析setup_paging子程序,其作用是设置页表,所以先将页表有关的定义贴出来:

    /*
     * I put the kernel page tables right after the page directory,
     * using 4 of them to span 16 Mb of physical memory. People with
     * more than 16MB will have to expand this.
     */
    .org 0x1000
    pg0:
    
    .org 0x2000
    pg1:
    
    .org 0x3000
    pg2:
    
    .org 0x4000
    pg3:
    
    .org 0x5000
    

    这里定义了四个页表,TIPS:程序开始部分我重点提醒了pg_dir,如果不记得,赶紧去上面看看,下面会用到。

    setup_paging函数:设置所有页表项内容:

    	/*
     * Setup_paging
     *
     * This routine sets up paging by setting the page bit
     * in cr0. The page tables are set up, identity-mapping
     * the first 16MB. The pager assumes that no illegal
     * addresses are produced (ie >4Mb on a 4Mb machine).
     *
     * NOTE! Although all physical memory should be identity
     * mapped by this routine, only the kernel page functions
     * use the >1Mb addresses directly. All "normal" functions
     * use just the lower 1Mb, or the local data space, which
     * will be mapped to some other place - mm keeps track of
     * that.
     *
     * For those with more memory than 16 Mb - tough luck. I've
     * not got it, why should you :-) The source is here. Change
     * it. (Seriously - it shouldn't be too difficult. Mostly
     * change some constants etc. I left it at 16Mb, as my machine
     * even cannot be extended past that (ok, but it was cheap :-)
     * I've tried to show which constants to change by having
     * some kind of marker at them (search for "16Mb"), but I
     * won't guarantee that's all :-( )
     */
    .align 2
    setup_paging:
    	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
    							# 一个表目录+4个页表 即五个表,每个表含有1024个表项,所以5*1024
    	xorl %eax,%eax
    	xorl %edi,%edi			/* pg_dir is at 0x000 */
    	cld;rep;stosl
    	# 设置4个表的地址+属性,这里的属性就是7:页存在,用户可读写,上面讲过
    	# 将四个页表的地址+属性装入页目录表项中
    	movl $pg0+7,pg_dir		/* set present bit/user r/w */
    	movl $pg1+7,pg_dir+4		/*  --------- " " --------- */
    	movl $pg2+7,pg_dir+8		/*  --------- " " --------- */
    	movl $pg3+7,pg_dir+12		/*  --------- " " --------- */
    	# 从后向前设置,最后一个表项所在地址为pg3+4092
    	# 一个页表有1024项,一项4字节,最后一个页表项地址为pg3+4092 -- pg3+4095
    	movl $pg3+4092,%edi
    	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */
    							# 16M-1是内存的末地址,一个页框占4096字节,16M-1=0xffffff,4KB=4096字节=0x1000
    							# 所以最后一个页框的地址是0xffffff-0x1000+1=0xfff000
    	std
    1:	stosl			/* fill pages backwards - more efficient :-) */
    	subl $0x1000,%eax			# 一个页表可寻址4k == 0x1000的地址空间,所以0x1000
    	jge 1b
    	# CR3是页目录基址寄存器,指向页目录表0x0000 ? 为什么是0x0000,也许因为pg_dir定义在本文件的开始出吧
    	xorl %eax,%eax		/* pg_dir is at 0x0000 */
    	movl %eax,%cr3		/* cr3 - page directory start */
    	movl %cr0,%eax
    	orl $0x80000000,%eax
    	movl %eax,%cr0		/* set paging (PG) bit */
    	ret			/* this also flushes prefetch-queue */ # ret后将会跳转至main函数继续执行。
    

    这里有疑问的可能就是0xfff007这里了,其实笔者也没弄明白16M - 4096是啥意思,读者如果明白的话,记得评论告诉我下(抱拳)。

    执行完ret后程序就跳转到main.c了,main.c见下一篇博客,后面就是C语言占主要部分啦!

    程序的最后部分是一些数据定义,笔者就不贴了,还是建议下载源代码结合本博客一起读。

第三章小结

  • bootsect.s程序的主要功能:将setup.s和system模块加载到内存中,并且将自身移动到0x90000处,然后控制权交给setup.s程序。
  • setup程序:利用BIOS获取硬件参数并保存(main.c会用到的);将system移动到0x00000;描述符表寄存器设置;硬件中断设置;设置CR0进入32位保护模式,控制权交给head.s。
  • head.s程序:设置中断描述符表项(哑中断);检查A20;测试是否有协处理器;初始化内存页目录表;跳转到main.c执行内核初始化。
  • main.c见下章,马上就能和亲切的C语言见面了。

本系列博客目录
下一篇传送门-第四章 内核初始化程序 init(main.c)

2019-09-19 17:31:27 zy_com 阅读数 18
  • 自己动手从0到1写嵌入式操作系统

    这不是rtos源码分析的课程,而是为初级的同学设计,从基础原理讲师,一步步不断迭代设计rtos的课程! 用不到【2000行代码,汇编代码仅18行】(不含注释)实现一个精巧的可以运行在ARM Cortex-M内核芯片上的RTOS! 该RTOS功能与ucos类似,具体实现不同。学习之后,再去学习ucos之类的系统将没有什么问题。

    9215 人正在学习 去看看 李述铜

1. 注释和续行符

  • python使用#开始注释 和Linux的一样
  • 如果一行语句很长 要分成多行 则需要使用\在每行最后续行
  • 当多行之间有括号等连接时 则不需要使用续行符
  • python使用换行结束一条语句
  • 同一行要写多个语句 则使用分隔即可

1. 判断语句

  • 语句格式
 if exp:
    syntax0
 elif:
 	syntax1
 	if exp1:
 		syntax3
 	else:
 		syntax4
 else:
    syntax5
  • 注意判断表达式没用括号括起来 遵循续行符规则
  • 判断表达式最后无论if还是else都必须要有结束
  • elif就是else if 同等缩进为同一条件结构 嵌套条件也是缩进实现

1. 语句块

  • 增加缩进表示增加语句块
  • 同级语句块 有相同缩进数量
  • 减少缩进 表示语句块的退出

1. 输入输出

  1. 输出

    • 使用print("exp") 或者print(var)输出字符串或者变量

    • print(var1,var2...)

    • 格式化输出 类似与C中的printf 但有两种方式 根据%隔开 或者用{[参数序号:参数格式]}+format输出

      Int:
          str='python'
          x=len(str)
          print('the length of %s is %d' %(str,x))
      Out:
          the length of python is 6
          
      Int:
          str='python'
          x=len(str)
          print('the length of %s is %d' %(str,x),end='--')
      Out:
          the length of python is 6--  #不带换行符   
      >>> pi = 3.141592653  
      >>> print('%10.3f' % pi) #字段宽10,精度3  
           3.142  
      >>> print("pi = %.*f" % (3,pi)) #用*从后面的元组中读取字段宽度或精度  
      pi = 3.142  
      >>> print('%010.3f' % pi) #用0填充空白  
      000003.142  
      >>> print('%-10.3f' % pi) #左对齐  
      3.142       
      >>> print('%+f' % pi) #显示正负号  
      +3.141593
      
      Int:
          a = 'hello'
          b = 10
          c = 1.23
          d = 2.34567
          print('{} 和 {} 两人去找{:f}家里找 {:.3f} 玩'.format(a, b, c, d))
      
      Out:
          hello 和 10 两人去找1.230000家里找 2.346
  2. 输入

    • 使用x=input() 或者带提示的输入x=input('enter a string')
    • input()函数返回值默认为字符串
    • 使用x=int(intput()) 或者带提示的输入x=int(input('enter a num'))更改输入为整型 依次类推

1. 安装包

pip install 包名


1. 运算符

  1. 算术运算符
    • 乘方**
    • 整除//
    • 其余和Java的一样
  2. 位运算符 和Java区别不大
  3. 比较运算符
    • Java区别不大
    • 支持连续比较 更人性化 如3<4<7 4<6==7
  4. 逻辑运算符 和jstl的类似 用英文单词not and or

1. 变量

  • python是动态的强类型语言
  • 变量第一次赋值 同时获得值和类型。不要显示指定类型
  • 引用的方式实现赋值 指向值所在的内存单元
    1. 支持增量赋值X+=5
    2. 支持链式赋值X=x=6 python区分大小写
    3. 支持多重赋值 等号两边以元组的方式出现
      • (x,y)=(1,2)
      • x,y=y,x
  • 作用域
    • 全局变量
      • 在主函数主体的变量是全局变量
      • 在自定义函数中 若用global关键字修饰的变量 也是全局变量
    • 局部变量
      • 在自定义函数和循环语句中有局部变量
      • 全局变量和局部变量同名时 局部变量可见。若要让全局变量可见 则用global关键字

  1. 数据类型(type(var/exp) 会显示类型)
    1. (长)整型(区分不大)
      • 范围:(几乎不会溢出)
      • 32位机器:-2^31~2^31-1
      • 64位机器:-2^63~2^63-1
    2. 浮点型
    3. 复数型
      • complex虚数部分必须有j
      • a+bj
      • 分离实数和虚数部分
        • .real
        • .imag
        • .conjugate()获得原复数的共轭复数
    4. 布尔型
      • True False
      • 本质存1 0
    5. 字符串
      • 'str'
      • "str"
      • '''str''' 由三个单引号构成的三引号,里面可以使用单双引号,而且多行时,不需要续行符
    6. 列表
      • 强大的类型 用方括号[]界别 属于可变类型
    7. 元组
      • 与列表类似 用小括号()界别 属于不可变类型
    8. 字典

1. 函数

  1. 内建函数

    • 无需导入可直接使用 比如help round range(var,[...])

    • 使用dir(__builtins__)查看所有内建函数以及常量 使用help(函数名)查看某个函数的帮助

    • range(start,end,step) range(start,end) range(end)后两者则默认步长是1 返回值是list对象

      [外链图片转存失败(img-q8rnBu24-1569199919845)(/home/yang/Desktop/%E6%B7%B1%E5%BA%A6%E5%BD%95%E5%B1%8F_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20190919160032.gif)]

  2. 实用函数

    1. 需要导入才可以使用
    2. 导入模块或者函数本身
      • 模块是一个完整的python文件 模块是对应与.py的物理组织方式 下的逻辑上的组织方式
      • python使用import 模块
      • 导入多个模块 import name1,name2,...
      • 导入模块中指定的属性 from Module1 import ModuleElement
    3. 导入包(有层次的文件目录结构)
      • 导入import AAA.BBB.CCC.c1 使用AAA.BBB.CCC.c1.func1()
      • 导入from AAA.BBB.CCC.c1 import func1 使用func1()
      • 是一组具有相关功能的模块集合
      • python的一大特色 是具有强大的标准库 第三方库 自定义模块
  3. 自定义函数

    1. 基础语法

      def function_name([args]):
          'optional doc string'
          function_body
      
      • 例子

        def add2(x):
            'apply operation + to arg'
            return x+x
        add2(3)
        
        -----6
        
        from math import sqrt
        def isprime(x):
            if x==1:
                return False
            k=int(sqrt(x))
            for j in range(2,k+1):
                if x%j==0:
                    return False
            return True
        for i in range(2,101):
            if isprime(i):
                print(i,end=' ')
                
        -----2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
        
    2. 默认参数

      • 函数参数可以有默认值 以赋值语句的形式提供

        def f(x=True):
            if x:
                print('word')
            print('Ok')
        f()
        ------word
              Ok
        f(False)
        ------Ok
        
      • 默认参数的值可以改变 在函数调用的时候(如果需要的话 一般提供了则不需要)

      • 默认参数一般要放在参数列表的最后 防止引起歧义

    3. 关键字参数

      • 就是函数调用传参数的时候 使用f(形参名字=value,...) 指明每个参数传入的值

      • 关键字参数让开发者可以使用参数名区分参数,并且可以改变参数顺序

      • 使用了关键字参数 则所有参数赋值都要用关键字参数 不允许部分

        #如上述质数函数调用 isprime(6)改成
        isprime(x=6)
        
    4. 传递参数

      • 把函数当做参数

        def add2(x):
            'apply operation + to arg'
            return x+x
        def self(f,y):
            print(f(y))
        self(add2,4)
        
        ------8
        
    5. lambda函数

      • 匿名函数的一种 和c++ java的类似

      • 定义

        function_name = args : function_body
            
        r=lambda x:x+x
        r(8)
        -----16
        
    6. 返回值

      1. 返回类型为none 则返回对象个数是0
      2. 返回类型为Object 则返回对象个数是1
      3. 返回类型是tuple 元组 则返回对象个数是>1 和Java那些很不一样

1. 循环

  1. while循环 结合if结构比较容易理解

    In:sumA=0
        j=1
        while j<10:
            sumA+=j
            j+=1
        sumA
    Out: 45
    
  2. for循环

    • 语法:

      for iter_var in iterable_Object:
          suite_to_repeat
         
      
      
      # iterable_Object包括String List Tuple Dictionary File
      
      # 用于遍历一个数据集内的成员
      In:s='python'
          for c in s:
              print(c,end='')
          print() #输出一个换行符
      
          for i in range(3,11,2):
              print(i,end=' ')
      
      Out:
      	python
      	3 5 7 9 
          
        
      # 在列表解析式中使用 返回一个列表 用[]括起来
      [i for i in range(10)]
      	[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      [i+1 for i in range(10) if i%2==0]
      	[1, 3, 5, 7, 9]
          
      # 在生成器表达式中使用 返回一个生成器 用()括起来 其他和在列表解析式中使用一致
      
  3. 结合Java 使用breakcontinue也差不多

    • pythonelse可以while循环一起使用 特殊

      • 表示 如果循环从break处终止 跳出循环

      • 如果正常结束循环 则执行else中的代码

        Int:
            from math import sqrt
            num=int(input('please enter a number:'))
            j=2
            while j<=int(sqrt(num)):
                if num %j==0:
                    print('{:d} is not a prime'.format(num))
                    break
                j+=1
            else:
                print('{:d} is a prime'.format(num))
                
        Out:
            please enter a number:26
        	26 is not a prime
        
  4. 和递归

    def fib1(x):
        a,b=0,1
        count=1
        while(count<x):
            a,b=b,a+b
            count+=1
        return a
        
    def fib2(x):
        if x==0 or x==1:
            return x
        else:
            return fib2(x-1)+fib2(x-2)
        
    def hanoi(a,b,c,n):
        if n==1:
            print(a,'->',c)
        else:
            hanoi(a,c,b,n-1)
            print(a,'->',c)
            hanoi(b,a,c,n-1)
    
2019-05-25 12:54:19 qq_36321889 阅读数 141
  • 自己动手从0到1写嵌入式操作系统

    这不是rtos源码分析的课程,而是为初级的同学设计,从基础原理讲师,一步步不断迭代设计rtos的课程! 用不到【2000行代码,汇编代码仅18行】(不含注释)实现一个精巧的可以运行在ARM Cortex-M内核芯片上的RTOS! 该RTOS功能与ucos类似,具体实现不同。学习之后,再去学习ucos之类的系统将没有什么问题。

    9215 人正在学习 去看看 李述铜

续上一篇 bootsect.s讲解

功能描述

  • setup程序的主要作用是利用ROM BIOS中断读取机器系统数据,并将这些数据保存到0x90000开始的位置
  • 为什么保存在0x90000开始的位置呢?因为0x90000之前保存的bootsect程序已经执行完毕,可以覆盖掉了
    在这里插入图片描述
  • 将system模块从0x10000-0x8ffff(512KB)整体向下移动到内存绝对地址0x00000处。
  • 加载中断描述符表寄存器idtr和全局描述符表寄存器gdtr,开启A20地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20 - 0x2f。
  • 最后设置CPU的控制寄存器CR0,从而进入32位保护模式运行,并跳转到位于system模块最前面部分的head.s继续运行。

代码分析

  1. 首先是数据声明部分
    INITSEG  = 0x9000	! we move boot here - out of the way
    SYSSEG   = 0x1000	! system loaded at 0x10000 (65536).
    SETUPSEG = 0x9020	! this is the current segment
    
    .globl begtext, begdata, begbss, endtext, enddata, endbss
    .text
    begtext:
    .data
    begdata:
    .bss
    begbss:
    .text
    
  2. 先读取当前坐标,保存起来以备后续使用
    entry start
    start:
    
    ! ok, the read went well so we get current cursor position and save it for
    ! posterity.
    
    	mov	ax,#INITSEG	! this is done in bootsect already, but...
    	mov	ds,ax
    	mov	ah,#0x03	! read cursor pos
    	xor	bh,bh
    	int	0x10		! save it in known place, con_init fetches
    	mov	[0],dx		! 返回结果保存在dx寄存器中,然后将结果保存到0x90000处。it from 0x90000.
    
  3. 获取从0x10000开始的扩展内存大小(KB)
    ! Get memory size (extended mem, kB)
    
    	mov	ah,#0x88
    	int	0x15
    	mov	[2],ax		! 返回值保存在ax中,ax=从0x100000开始的扩展内存大小(KB)
    
  4. 获取显卡相关信息并保存起来
    ! Get video-card data:
    
    	mov	ah,#0x0f
    	int	0x10
    	mov	[4],bx		! bh = display page ,当前页数
    	mov	[6],ax		! al = video mode 显示模式, ah = window width 字符列数
    
  5. 获取VGA相关信息,并保存起来
    ! check for EGA/VGA and some config parameters
    
    	mov	ah,#0x12
    	mov	bl,#0x10
    	int	0x10
    	mov	[8],ax
    	mov	[10],bx		! 0x9000A = 安装的显示内存, 0x9000B = 显示模式(彩色/单色)
    	mov	[12],cx 	! 显示卡特性参数
    
  6. 获取硬盘相关的信息,并保存
    ! Get hd0 data    	! 取0号硬盘的参数表,是中断0x41的向量值,一共取0x10个字节
    
    	mov	ax,#0x0000
    	mov	ds,ax
    	lds	si,[4*0x41]
    	mov	ax,#INITSEG
    	mov	es,ax
    	mov	di,#0x0080
    	mov	cx,#0x10
    	rep
    	movsb
    
    ! Get hd1 data		! 取1号硬盘的参数表,是中断0x46的向量值,一共取0x10个字节
    
    	mov	ax,#0x0000
    	mov	ds,ax
    	lds	si,[4*0x46]
    	mov	ax,#INITSEG
    	mov	es,ax
    	mov	di,#0x0090
    	mov	cx,#0x10
    	rep
    	movsb
    ! Check that there IS a hd1 :-)
    
    	mov	ax,#0x01500
    	mov	dl,#0x81
    	int	0x13
    	jc	no_disk1
    	cmp	ah,#3			! ah==3表示是硬盘
    	je	is_disk1
    no_disk1:				! 如果不是硬盘则对hd1硬盘表清0
    	mov	ax,#INITSEG
    	mov	es,ax
    	mov	di,#0x0090
    	mov	cx,#0x10
    	mov	ax,#0x00
    	rep
    	stosb				! 该指令的作用:将al赋值给es:di
    is_disk1:
    
  7. 准备进入保护模式了,先将系统起始地址从0x10000移动到0x00000
    ! now we want to move to protected mode ...
    ! 进入保护模式的相关工作
    
    	cli			! no interrupts allowed ! 关中断
    
    ! first we move the system to it's rightful place
    
    	mov	ax,#0x0000
    	cld			! 'direction'=0, movs moves forward,清方向标志,表示每次移动si+=1, di+=1
    do_move:
    	mov	es,ax		! destination segment
    	add	ax,#0x1000	! 每次循环ax+=0x1000,即每次移动一个段=64kb
    	cmp	ax,#0x9000	!判断是否移动完毕
    	jz	end_move
    	mov	ds,ax		! source segment
    	sub	di,di
    	sub	si,si
    	mov cx,#0x8000	! 这里计算一下:0x8000次,由于使用movsw即每次双字节,那么0x8000*2=0x10000=64kb=段大小
    	rep
    	movsw
    	jmp	do_move
    
  8. 加载中断描述符表寄存器和全局描述符表寄存器
    ! then we load the segment descriptors
    
    end_move:
    	mov	ax,#SETUPSEG	! right, forgot this at first. didn't work :-),回到本程序所在的段
    	mov	ds,ax
    	lidt	idt_48		! load idt with 0,0  将48位中断描述符表寄存器加载到idt_48变量处,在后面定义
    	lgdt	gdt_48		! load gdt with whatever appropriate,同上
    
  9. 打开A20地址线,关于A20地址线:参考博客, A20是一个为了解决80286的一个bug引入的,之后为了实现向下兼容而一直保留。
    ! that was painless, now we enable A20
    
    call	empty_8042 	! 必须等待缓冲区为空
    mov	al,#0xD1		! command write,发送禁止键盘操作命令
    out	#0x64,al	
    call	empty_8042	! 再次等待缓冲区为空
    mov	al,#0xDF		! A20 on, 将A20打开的命令
    out	#0x60,al
    call	empty_8042	! 等待到缓冲区为空,A20就打开了
    
  10. 下面设置中断,为了避免与保留的硬件中断发生冲突,所以中断的起始号是0x20。因为IBM并没有这么做,所以我们需要重新对8259芯片编程,下面代码都是操作8259的,笔者没有仔细去研究,对8259感兴趣的读者可以去查一下关于8259的资料
    ! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
    ! we put them right after the intel-reserved hardware interrupts, at
    ! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
    ! messed this up with the original PC, and they haven't been able to
    ! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
    ! which is used for the internal hardware interrupts as well. We just
    ! have to reprogram the 8259's, and it isn't fun.
    
    	mov	al,#0x11		! initialization sequence
    	out	#0x20,al		! send it to 8259A-1
    	.word	0x00eb,0x00eb		! jmp $+2, jmp $+2, 这里其实是两个指令,用来起延时作用的
    	out	#0xA0,al		! and to 8259A-2
    	.word	0x00eb,0x00eb
    	mov	al,#0x20		! start of hardware int's (0x20)
    	out	#0x21,al
    	.word	0x00eb,0x00eb
    	mov	al,#0x28		! start of hardware int's 2 (0x28)
    	out	#0xA1,al
    	.word	0x00eb,0x00eb
    	mov	al,#0x04		! 8259-1 is master
    	out	#0x21,al
    	.word	0x00eb,0x00eb
    	mov	al,#0x02		! 8259-2 is slave
    	out	#0xA1,al
    	.word	0x00eb,0x00eb
    	mov	al,#0x01		! 8086 mode for both
    	out	#0x21,al
    	.word	0x00eb,0x00eb
    	out	#0xA1,al
    	.word	0x00eb,0x00eb
    	mov	al,#0xFF		! mask off all interrupts for now
    	out	#0x21,al
    	.word	0x00eb,0x00eb
    	out	#0xA1,al
    
  11. 开启保护模式(很简单的操作,3行代码搞定),就是将CR0的PE位置1
    ! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
    ! need no steenking BIOS anyway (except for the initial loading :-).
    ! The BIOS-routine wants lots of unnecessary data, and it's less
    ! "interesting" anyway. This is how REAL programmers do it.
    !
    ! Well, now's the time to actually move into protected mode. To make
    ! things as simple as possible, we do no register set-up or anything,
    ! we let the gnu-compiled 32-bit programs do that. We just jump to
    ! absolute address 0x00000, in 32-bit protected mode.
    	mov	ax,#0x0001	! protected mode (PE) bit
    	lmsw	ax		! This is it! LMSW:Load Machine State Word 加载机器状态字,将PE位置1,开启保护模式
    	jmpi	0,8		! jmp offset 0 of segment 8 (cs), 这里是段选择符
    
    解析:
    • lmsw:Load Machine State World。其中机器状态字的0位为PE位,为保护模式的标志,所以执行这条指令就能打开保护模式。
    • 打开保护模式后,需要跳转到system代码处执行,因为打开了保护模式所以跳转指令也不一样了: jmpi 0, 8。
    • 上面的0表示偏移为0,8表示段选择符。其中位0-1表示特权级,这里为系统级特权。位2表示使用全局描述符表。位3-15表示描述符表项的索引值,这里为1,表示使用的是下文代码中的第一个描述符,其基址是0。表示跳转到段基址为0,偏移为0处继续执行。
  12. 剩余代码
    ! This routine checks that the keyboard command queue is empty
    ! No timeout is used - if this hangs there is something wrong with
    ! the machine, and we probably couldn't proceed anyway.
    empty_8042:
    	.word	0x00eb,0x00eb ! 两个跳转指令,起到延时的作用
    	in	al,#0x64	! 8042 status port
    	test	al,#2		! is input buffer full?
    	jnz	empty_8042	! yes - loop
    	ret
    
    gdt:
    	.word	0,0,0,0		! dummy ! 这是一个没用的描述符,但是必须有
    	! 第一个描述符
    	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
    	.word	0x0000		! base address=0  ! 段基址
    	.word	0x9A00		! code read/exec  ! 代码段
    	.word	0x00C0		! granularity=4096, 386
    	! 第二个描述符
    	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
    	.word	0x0000		! base address=0	! 段基址
    	.word	0x9200		! data read/write	! 数据段
    	.word	0x00C0		! granularity=4096, 386
    
    idt_48:
    	.word	0			! idt limit=0
    	.word	0,0			! idt base=0L
    
    gdt_48:
    	.word	0x800		! gdt limit=2048, 256 GDT entries
    	.word	512+gdt,0x9	! gdt base = 0X9xxxx
    	
    .text
    endtext:
    .data
    enddata:
    .bss
    endbss:
    

上面涉及描述符表的,看这张图:
在这里插入图片描述
涉及到的额外的知识:

  • BIOS int 0x10
  • 硬盘基本参数(int 0x41)
  • A20地址问题
  • 8259中断控制芯片
  • Intel CPU 32位保护运行模式,包括段选择符,段描述符,页表寻址机制等。

本系列博客目录
下一篇:head.s程序分析

2010-05-30 20:33:00 learnhard 阅读数 703
  • 自己动手从0到1写嵌入式操作系统

    这不是rtos源码分析的课程,而是为初级的同学设计,从基础原理讲师,一步步不断迭代设计rtos的课程! 用不到【2000行代码,汇编代码仅18行】(不含注释)实现一个精巧的可以运行在ARM Cortex-M内核芯片上的RTOS! 该RTOS功能与ucos类似,具体实现不同。学习之后,再去学习ucos之类的系统将没有什么问题。

    9215 人正在学习 去看看 李述铜

最近遇到了一个诡异的问题:将原来能编译通过,并且只改动了一丁点的代码,再传到Linux虚拟机上编译的时候,编译出错,而且是匪夷所思的错误。不得其解时,尝试了若干解决办法,包括大段大段地注释掉怀疑有问题的地方,但是最后把注释掉的代码全部放开的时候,仍然出错。于是到Linux下用Emacs查看源文件,非常吃惊:传上去的文件竟然与Windows下的文件不一样!

难道是上传的时候出错了吗?最后查出来的原因是:我用FTPRush(通过SFTP)来传送文件到Linux虚拟机里,尽管设置成了在任何情况下都将目标文件“覆盖”,但是仍然是类似于“续传”式的结果,即:在目标代码文件比较小的时候,本地文件多出来的最后那一部分代码就被FTPRush添加到了目标文件的末尾,使得括号或其他东西不匹配,使得编译通不过。

如果我先删除了目标文件,再传送过去,就没有这个问题了。

我用SSH Secure File Transfer Client就没有这个问题。可见,要当心SFTP客户端的续传问题,可能是我设置不当,表面看起来正确的设置,实际上是有问题的。

 

makefile设计规则

阅读数 21

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