zoukankan      html  css  js  c++  java
  • uboot启动linux的过程

     一、概述

      linux内核镜像常见到的有两种形式,zImage和uImage。这两种文件的格式稍有差别,所以启动这两种格式的内核镜像也会有所不同。目前,uboot只支持启动uImage类型的镜像,对zImage还不支持(但是可以移植,TQ2440就是这样做的)。

    二、uImage和zImage

    1、zImage

           zImage是用命令“#make zImage”生成的,我截取了生成信息最后部分的内容如下:

      OBJCOPY arch/arm/boot/Image
      Kernel: arch/arm/boot/Image is ready
      GZIP    arch/arm/boot/compressed/piggy.gz
      AS      arch/arm/boot/compressed/piggy.o
      LD      arch/arm/boot/compressed/vmlinux
      OBJCOPY arch/arm/boot/zImage
      Kernel: arch/arm/boot/zImage is ready

      从中可以看到,zImage是经过gzip压缩过的,所以在内核启动过程(不属于u-boot控制范围,在内核镜像的头部嵌有解压函数)中必然会对应一个解压过程。

    2、uImage

    (1) 生成方法

      uImage是u-boot专用的内核镜像,可用命令“#make uImage”生成。生成信息最后部分的内容如下:

      Kernel: arch/arm/boot/Image is ready
      Kernel: arch/arm/boot/zImage is ready
      UIMAGE  arch/arm/boot/uImage
    Image Name:   Linux-2.6.30.4-EmbedSky
    Created:      Thu Mar 20 19:53:32 2014
    Image Type:   ARM Linux Kernel Image (uncompressed)
    Data Size:    2314736 Bytes = 2260.48 kB = 2.21 MB
    Load Address: 0x30008000
    Entry Point:  0x30008000
      Image arch/arm/boot/uImage is ready

      事实上,uImage是调用mkimage(uboot制作的工具)这个工具生成的。

    root@daneiqi:/opt/EmbedSky#  mkimage -n 'linux-2.6.30' -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008000 -d zImage uImage
    Image Name:   linux-2.6.30
    Created:      Thu Mar 20 19:59:36 2014
    Image Type:   ARM Linux Kernel Image (uncompressed)
    Data Size:    2314736 Bytes = 2260.48 kB = 2.21 MB
    Load Address: 0x30008000
    Entry Point:  0x30008000

    (2)特点

      在原来的可执行映象文件zImage的前面加上一个0x40字节的头, 记录参数所指定的信息,这样uboot才能识别这个映象是针对哪个CPU体系结构的,哪个OS的, 哪种类型,加载内存中的哪个位置,入口点在内存的那个位置以及映象名是什么。

    (3)image_header

      头部的结构是在include/image.h中定义的,如下所示:

    typedef struct image_header {
           uint32_t  ih_magic;       /* Image Header Magic Number   */
           uint32_t  ih_hcrc;   /* Image Header CRC Checksum  */
           uint32_t  ih_time;  /* Image Creation Timestamp       */
           uint32_t  ih_size;   /* Image Data Size        */
           uint32_t  ih_load;   /* Data    Load  Address            */
           uint32_t  ih_ep;            /* Entry Point Address          */
           uint32_t  ih_dcrc;   /* Image Data CRC Checksum      */
           uint8_t           ih_os;             /* Operating System             */
           uint8_t           ih_arch;   /* CPU architecture              */
           uint8_t           ih_type;   /* Image Type               */
           uint8_t           ih_comp; /* Compression Type            */
           uint8_t           ih_name[IH_NMLEN];  /* Image Name             */
    } image_header_t;

      打开上边生成的uImage文件,可以看到对应的数据。

    (1)ih_magic    0x27051956  magic值,我觉得是uImage的头部开始值,根据这个值,判断是否是uImage

    (2)ih_crc    0x19dbf9c6    头部校验

    (3)ih_time   0x74295319   创建时间

    (4)ih_size   0x002351f0     镜像大小为2260.48KB

    (5)ih_load  0x30008000 内核加载地址

    (6)ih_ep        0x30008000 内核运行地址,“theKernel”指向该地址,说明这里藏着进入第一个函数--解压

    (7)ih_dcrc      0x38fc654e    内核校验

    (8)ih_os        0x05       #define IH_OS_LINUX  5 /* Linux */

    (9)ih_arch     0x02     #define IH_CPU_ARM  2 /* ARM  */

    (10)ih_type   0x02         #define IH_TYPE_KERNEL  2 /* OS Kernel Image  */

    (11)ih_comp  0x00        #define IH_COMP_NONE  0 /*  No  Compression Used */

    (12)ih_name         Linux_2.6.30.4-EmbedSky

    三、u-boot内核启动流程概述

      前文已经说明u-boot只支持uImage,步骤三、四都是针对uImage的。

      另外声明一点,步骤三四的测试uboot代码是韦东山视频提供的。

    1、从NandFlash中读取内核到RAM中

    2、在RAM中,给内核进行重定位

    3、给内核传递参数

    4、启动内核

    四、u-boot启动内核细节分析

    1、启动命令

    从环境变量中查看启动命令:

    2、从NandFlash中读取内核到RAM中

      nand read.jffs2 0x30007FC0 kernel

      此命令会激活(common/cmd_nand.c)中的do_nand函数,从而将nandflash上的kernel分区加载到0x30007fc0位置处。

    OpenJTAG> mtd
    
    device nand0 <nandflash0>, # parts = 4
     #: name                        size            offset          mask_flags
     0: bootloader          0x00040000      0x00000000      0
     1: params              0x00020000      0x00040000      0
     2: kernel              0x00200000      0x00060000      0
     3: root                0x0fda0000      0x00260000      0
    
    active partition: nand0,0 - (bootloader) 0x00040000 @ 0x00000000
    
    defaults:
    mtdids  : nand0=nandflash0
    mtdparts: mtdparts=nandflash0:256k@0(bootloader),128k(params),2m(kernel),-(root)

      从分区表中,可以看出kernel分区的起始地址是0x60000,大小是0x200000(2M),这条命令实际上等效于

    nand read.jffs2 0x30007FC0 0x60000 0x200000

      也可以使用命令

    nand read 0x30007FC0 0x60000 0x200000

      nand read.jffs2可以自动页对齐,所以大小可以是非页整的;如果使用nand read的大小必须是页对齐的。

    3、读取uImage头部

      bootm 0x30007fc0

      此命令会激活(common/cmd_bootm.c)中的do_bootm函数,从而开始执行

    2、在RAM中,给内核进行重定位
    3、给内核传递参数
    4、启动内核

    image_header_t header;  定义一个全局变量header,是读取头部的缓冲区

    addr = simple_strtoul(argv[1], NULL, 16);  定位头部地址,将字符串“0x30007fc0”转化为整型

    printf ("## Booting image at %08lx ... ", addr); 显示从哪儿启动

    memmove (&header, (char *)addr, sizeof(image_header_t)); 读取头部到header变量中

    4、判断当前的内存区是否是uImage的开始位置

     if (ntohl(hdr->ih_magic) != IH_MAGIC) {
          {
      puts ("Bad Magic Number
    ");
      SHOW_BOOT_PROGRESS (-1);
      return 1;
         }
     }

    注意到:

    #define IH_MAGIC 0x27051956 /* Image Magic Number  */(include/image.h)

    5、校验头部

        data = (ulong)&header;
        len  = sizeof(image_header_t);
    
        checksum = ntohl(hdr->ih_hcrc);
        hdr->ih_hcrc = 0;
    
        if (crc32 (0, (uchar *)data, len) != checksum) {
            puts ("Bad Header Checksum
    ");
            SHOW_BOOT_PROGRESS (-2);
            return 1;
        }

    6、打印头部信息

        /* for multi-file images we need the data part, too */
        print_image_hdr ((image_header_t *)addr);

    7、核查内核数据

        data = addr + sizeof(image_header_t);
        len  = ntohl(hdr->ih_size);
    
        if (verify) {
            puts ("   Verifying Checksum ... ");
            if (crc32 (0, (uchar *)data, len) != ntohl(hdr->ih_dcrc)) {
                printf ("Bad Data CRC
    ");
                SHOW_BOOT_PROGRESS (-3);
                return 1;
            }
            puts ("OK
    ");
        }
        SHOW_BOOT_PROGRESS (4);

      注意到data已经跳过了uImage的头部,指向了真正的内核首部,也即0x30008000。

    8、核查架构、内核类型、压缩类型等信息,其中会涉及到重定位

        len_ptr = (ulong *)data;
    
    #if defined(__PPC__)
        if (hdr->ih_arch != IH_CPU_PPC)
    #elif defined(__ARM__)
        if (hdr->ih_arch != IH_CPU_ARM)
    #elif defined(__I386__)
        if (hdr->ih_arch != IH_CPU_I386)
    #elif defined(__mips__)
        if (hdr->ih_arch != IH_CPU_MIPS)
    #elif defined(__nios__)
        if (hdr->ih_arch != IH_CPU_NIOS)
    #elif defined(__M68K__)
        if (hdr->ih_arch != IH_CPU_M68K)
    #elif defined(__microblaze__)
        if (hdr->ih_arch != IH_CPU_MICROBLAZE)
    #elif defined(__nios2__)
        if (hdr->ih_arch != IH_CPU_NIOS2)
    #elif defined(__blackfin__)
        if (hdr->ih_arch != IH_CPU_BLACKFIN)
    #elif defined(__avr32__)
        if (hdr->ih_arch != IH_CPU_AVR32)
    #else
    # error Unknown CPU type
    #endif
        {
            printf ("Unsupported Architecture 0x%x
    ", hdr->ih_arch);
            SHOW_BOOT_PROGRESS (-4);
            return 1;
        }
        SHOW_BOOT_PROGRESS (5);
    
        switch (hdr->ih_type) {
        case IH_TYPE_STANDALONE:
            name = "Standalone Application";
            /* A second argument overwrites the load address */
            if (argc > 2) {
                hdr->ih_load = htonl(simple_strtoul(argv[2], NULL, 16));
            }
            break;
        case IH_TYPE_KERNEL:
            name = "Kernel Image";
            break;
        case IH_TYPE_MULTI:
            name = "Multi-File Image";
            len  = ntohl(len_ptr[0]);
            /* OS kernel is always the first image */
            data += 8; /* kernel_len + terminator */
            for (i=1; len_ptr[i]; ++i)
                data += 4;
            break;
        default: printf ("Wrong Image Type for %s command
    ", cmdtp->name);
            SHOW_BOOT_PROGRESS (-5);
            return 1;
        }
        SHOW_BOOT_PROGRESS (6);
    
        /*
         * We have reached the point of no return: we are going to
         * overwrite all exception vector code, so we cannot easily
         * recover from any failures any more...
         */
    
        iflag = disable_interrupts();
    
    #ifdef CONFIG_AMIGAONEG3SE 
        /*
         * We've possible left the caches enabled during
         * bios emulation, so turn them off again
         */
        icache_disable();  
        invalidate_l1_instruction_cache();
        flush_data_cache();
        dcache_disable();
    #endif
    
        switch (hdr->ih_comp) {
        case IH_COMP_NONE:
            if(ntohl(hdr->ih_load) == data) {
                printf ("   XIP %s ... ", name);
            } else {
    #if defined(CONFIG_HW_WATCHDOG) || defined(CONFIG_WATCHDOG)
                size_t l = len;
                void *to = (void *)ntohl(hdr->ih_load);
                void *from = (void *)data;
    
                printf ("   Loading %s ... ", name);
    
                while (l > 0) {
                    size_t tail = (l > CHUNKSZ) ? CHUNKSZ : l;
                    WATCHDOG_RESET();
                    memmove (to, from, tail);
                    to += tail;
                    from += tail;
                    l -= tail;
                }
    #else    /* !(CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG) */
                memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);
    #endif    /* CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG */
            }
            break;
        case IH_COMP_GZIP:
            printf ("   Uncompressing %s ... ", name);
            if (gunzip ((void *)ntohl(hdr->ih_load), unc_len,
                    (uchar *)data, &len) != 0) {
                puts ("GUNZIP ERROR - must RESET board to recover
    ");
                SHOW_BOOT_PROGRESS (-6);
                do_reset (cmdtp, flag, argc, argv);
            }
            break;
    #ifdef CONFIG_BZIP2
        case IH_COMP_BZIP2:
            printf ("   Uncompressing %s ... ", name);
            /*
             * If we've got less than 4 MB of malloc() space,
             * use slower decompression algorithm which requires
             * at most 2300 KB of memory.
             */
            i = BZ2_bzBuffToBuffDecompress ((char*)ntohl(hdr->ih_load),
                            &unc_len, (char *)data, len,
                            CFG_MALLOC_LEN < (4096 * 1024), 0);
            if (i != BZ_OK) {
                printf ("BUNZIP2 ERROR %d - must RESET board to recover
    ", i);
                SHOW_BOOT_PROGRESS (-6);
                udelay(100000);
                do_reset (cmdtp, flag, argc, argv);
            }
            break;
    #endif /* CONFIG_BZIP2 */
        default:
            if (iflag)
                enable_interrupts();
            printf ("Unimplemented compression type %d
    ", hdr->ih_comp);
            SHOW_BOOT_PROGRESS (-7);
            return 1;
        }
        puts ("OK
    ");
        SHOW_BOOT_PROGRESS (7);
    
        switch (hdr->ih_type) {
        case IH_TYPE_STANDALONE:
            if (iflag)
                enable_interrupts();
    
            /* load (and uncompress), but don't start if "autostart"
             * is set to "no"
             */
            if (((s = getenv("autostart")) != NULL) && (strcmp(s,"no") == 0)) {
                char buf[32];
                sprintf(buf, "%lX", len);
                setenv("filesize", buf);
                return 0;
            }
            appl = (int (*)(int, char *[]))ntohl(hdr->ih_ep);
            (*appl)(argc-1, &argv[1]);
            return 0;
        case IH_TYPE_KERNEL:
        case IH_TYPE_MULTI:
            /* handled below */
            break;
        default:
            if (iflag)
                enable_interrupts();
            printf ("Can't boot image type %d
    ", hdr->ih_type);
            SHOW_BOOT_PROGRESS (-8);
            return 1;
        }
        SHOW_BOOT_PROGRESS (8);
    View Code

      在这部分代码中,有这么一部分关于压缩类型的:

        switch (hdr->ih_comp) {
        case IH_COMP_NONE:
            if(ntohl(hdr->ih_load) == data) {
                printf ("   XIP %s ... ", name);
            } else {
    #if defined(CONFIG_HW_WATCHDOG) || defined(CONFIG_WATCHDOG)
                size_t l = len;
                void *to = (void *)ntohl(hdr->ih_load);
                void *from = (void *)data;
    
                printf ("   Loading %s ... ", name);
    
                while (l > 0) {
                    size_t tail = (l > CHUNKSZ) ? CHUNKSZ : l;
                    WATCHDOG_RESET();
                    memmove (to, from, tail);
                    to += tail;
                    from += tail;
                    l -= tail;
                }
    #else    /* !(CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG) */
                memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);
    #endif    /* CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG */
            }
            break;

      可以看到,u-boot会判断当前去除uImage头部内核代码所处的位置(7步骤已经说明地址是data)是否与编译时安排的重定位位置(hdr->ih_load)一致。

      如果一致,就打印一句话。

      如果不一致,则需要调用 memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);进行内核的重定位,要知道它有2M多的大小,会花费一些时间。尽量使读取内核的时候,就读取到hdr->ih_load-64的位置上,这样就不必再搬运一次。

    9、根据操作系统类型,启动对应的操作系统

        switch (hdr->ih_os) {
        default:            /* handled by (original) Linux case */
        case IH_OS_LINUX:
    #ifdef CONFIG_SILENT_CONSOLE
            fixup_silent_linux();
    #endif
            do_bootm_linux  (cmdtp, flag, argc, argv,
                     addr, len_ptr, verify);
            break;
        case IH_OS_NETBSD:   

    10、执行do_bootm_linux,继续启动linux系统

      此函数在lib_arm/armlinux.c中

        void (*theKernel)(int zero, int arch, uint params);
        image_header_t *hdr = &header;
        bd_t *bd = gd->bd;
    
    #ifdef CONFIG_CMDLINE_TAG
        char *commandline = getenv ("bootargs");
    #endif
    
        theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);

      可见,已经将内核运行的首地址赋给了theKernel函数指针变量,将来可以利用这个变量调用进入内核的函数。

      另外,在进入内核之前,要给内核传递参数。方法是将参数以一定的结构放在内存指定的位置上,将来内核从该地址读取数据即可。

      命令行的启动参数存储在以bootargs命名的对象里,值为

    bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

      告诉内核,启动后的根文件系统位于mtd的哪个区,初始进程,以及控制台

    11、判断是否是一个ramdisk或者multi镜像

        /*
         * Check if there is an initrd image
         */
        if (argc >= 3) {
            SHOW_BOOT_PROGRESS (9);
    
            addr = simple_strtoul (argv[2], NULL, 16);
    
            printf ("## Loading Ramdisk Image at %08lx ...
    ", addr);
    
            /* Copy header so we can blank CRC field for re-calculation */
    #ifdef CONFIG_HAS_DATAFLASH
            if (addr_dataflash (addr)) {
                read_dataflash (addr, sizeof (image_header_t),
                        (char *) &header);
            } else
    #endif
                memcpy (&header, (char *) addr,
                    sizeof (image_header_t));
    
            if (ntohl (hdr->ih_magic) != IH_MAGIC) {
                printf ("Bad Magic Number
    ");
                SHOW_BOOT_PROGRESS (-10);
                do_reset (cmdtp, flag, argc, argv);
            }
    
            data = (ulong) & header;
            len = sizeof (image_header_t);
    
            checksum = ntohl (hdr->ih_hcrc);
            hdr->ih_hcrc = 0;
    
            if (crc32 (0, (unsigned char *) data, len) != checksum) {
                printf ("Bad Header Checksum
    ");
                SHOW_BOOT_PROGRESS (-11);
                do_reset (cmdtp, flag, argc, argv);
            }
    
            SHOW_BOOT_PROGRESS (10);
    
            print_image_hdr (hdr);
    
            data = addr + sizeof (image_header_t);
            len = ntohl (hdr->ih_size);
    
    #ifdef CONFIG_HAS_DATAFLASH
            if (addr_dataflash (addr)) {
                read_dataflash (data, len, (char *) CFG_LOAD_ADDR);
                data = CFG_LOAD_ADDR;
            }
    #endif
    
            if (verify) {
                ulong csum = 0;
    
                printf ("   Verifying Checksum ... ");
                csum = crc32 (0, (unsigned char *) data, len);
                if (csum != ntohl (hdr->ih_dcrc)) {
                    printf ("Bad Data CRC
    ");
                    SHOW_BOOT_PROGRESS (-12);
                    do_reset (cmdtp, flag, argc, argv);
                }
                printf ("OK
    ");
            }
    
            SHOW_BOOT_PROGRESS (11);
    
            if ((hdr->ih_os != IH_OS_LINUX) ||
                (hdr->ih_arch != IH_CPU_ARM) ||
                (hdr->ih_type != IH_TYPE_RAMDISK)) {
                printf ("No Linux ARM Ramdisk Image
    ");
                SHOW_BOOT_PROGRESS (-13);
                do_reset (cmdtp, flag, argc, argv);
            }
    
    #if defined(CONFIG_B2) || defined(CONFIG_EVB4510) || defined(CONFIG_ARMADILLO)
            /*
             *we need to copy the ramdisk to SRAM to let Linux boot
             */
            memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);
            data = ntohl(hdr->ih_load);
    #endif /* CONFIG_B2 || CONFIG_EVB4510 */
    
            /*
             * Now check if we have a multifile image
             */
        } else if ((hdr->ih_type == IH_TYPE_MULTI) && (len_ptr[1])) {
            ulong tail = ntohl (len_ptr[0]) % 4;
            int i;
    
            SHOW_BOOT_PROGRESS (13);
    
            /* skip kernel length and terminator */
            data = (ulong) (&len_ptr[2]);
            /* skip any additional image length fields */
            for (i = 1; len_ptr[i]; ++i)
                data += 4;
            /* add kernel length, and align */
            data += ntohl (len_ptr[0]);
            if (tail) {
                data += 4 - tail;
            }
    
            len = ntohl (len_ptr[1]);
    
        } else {
            /*
             * no initrd image
             */
            SHOW_BOOT_PROGRESS (14);
    
            len = data = 0;
        }
    
    #ifdef    DEBUG
        if (!data) {
            printf ("No initrd
    ");
        }
    #endif
    View Code

    12、给内核传递参数

    #if defined (CONFIG_SETUP_MEMORY_TAGS) || 
        defined (CONFIG_CMDLINE_TAG) || 
        defined (CONFIG_INITRD_TAG) || 
        defined (CONFIG_SERIAL_TAG) || 
        defined (CONFIG_REVISION_TAG) || 
        defined (CONFIG_LCD) || 
        defined (CONFIG_VFD)
        setup_start_tag (bd);
    #ifdef CONFIG_SERIAL_TAG
        setup_serial_tag (&params);
    #endif
    #ifdef CONFIG_REVISION_TAG
        setup_revision_tag (&params);
    #endif
    #ifdef CONFIG_SETUP_MEMORY_TAGS
        setup_memory_tags (bd);
    #endif
    #ifdef CONFIG_CMDLINE_TAG
        setup_commandline_tag (bd, commandline);
    #endif
    #ifdef CONFIG_INITRD_TAG
        if (initrd_start && initrd_end)
            setup_initrd_tag (bd, initrd_start, initrd_end);
    #endif
    #if defined (CONFIG_VFD) || defined (CONFIG_LCD)
        setup_videolfb_tag ((gd_t *) gd);
    #endif
        setup_end_tag (bd);
    #endif

      比较重要的函数有:

       setup_start_tag (bd);

      setup_memory_tags (bd);

      setup_commandline_tag (bd, commandline);

      setup_end_tag (bd);

      其中 bd->bi_boot_params(参考uboot全局变量),bi_boot_params=>>0x30000100,启动参数存放的位置。

    13、启动内核

        printf ("
    Starting kernel ...
    
    ");
      theKernel (0, bd->bi_arch_number, bd->bi_boot_params);

      把机器码以及启动参数存放的位置都告诉给内核。

    五、启动过程展示

    1、不需要重定位启动

    2、重定位启动

          下例中读取到的位置,不是合适的位置,内核的入口不是0x30008000,所以还要对内核进行重定位,也就是将内核搬移到指定的位置。

    六、u-boot启动zImage

     1、直接启动zImage

      既然,zImage是uImage去除头部的部分,那么可以从0x30008000直接启动zImage,我们用go命令去执行。

    可见,内核的第一个函数果然是解压函数。但是程序卡到图片最后的位置,不能继续执行。

      原因是由于没有给内核传递启动参数,也就是说在执行函数theKernel之前,没有做好准备

    void (*theKernel)(int zero, int arch, uint params);

    2、移植u-boot支持启动zImage

      具体代码可看TQ2440开发板的u-boot代码。

      再来看一下启动大纲:

    1、从NandFlash中读取内核到RAM中
    
    2、在RAM中,给内核进行重定位
    
    3、给内核传递参数
    
    4、启动内核

      可以直接从nandflash中将内核zImage读取到内存0x30008000位置处,然后在0x30000100位置处传递参数

    也就是调用函数 

    setup_start_tag (bd);
    setup_memory_tags (bd);
    setup_commandline_tag (bd, commandline);
    setup_end_tag (bd);

      最后,调用theKernel函数启动内核。

    参考资料:韦东山u-boot启动内核视频

         uboot全局变量

           linux的uboot启动映像、zImage和uImage的区别

  • 相关阅读:
    《算法》第二章部分程序 part 3
    《算法》第二章部分程序 part 2
    《算法》第二章部分程序 part 1
    《算法》第一章部分程序 part 2
    《算法》第一章部分程序 part 1
    Java,Hello World,《算法》环境搭建中的问题,用 cmd 和 IntelliJ Idea 分别编译和运行 Java 程序
    《汇编语言 基于x86处理器》第十一章 MS-DOS 编程部分的代码 part 1
    《汇编语言 基于x86处理器》第十三章高级语言接口部分的代码 part 2
    《汇编语言 基于x86处理器》第十三章高级语言接口部分的代码 part 1
    《汇编语言》(王爽)补充笔记,第 14 ~ 17 章
  • 原文地址:https://www.cnblogs.com/amanlikethis/p/3614594.html
Copyright © 2011-2022 走看看