hex文件手动合并成烧写,x64编程基础

最近在将APP和boot合并,一般手动合并是将boot里的.hex结束语句:0000001FF和:04000005000000c136成的开始线性地址记录移除之后复制粘贴APP的.hex内容。突然发现合并后的.hex的0开始线性地址记录是用.APP的:040000050000020c116,后来经过看他的.map文件发现这句话记录的是_main.o地址,是不是就表明单片机运行只会从APP的main开始运行而不是boot里的main开始运行,这样不就造成boot被跳过没用了吗?

选择编译器

nasm?fasm?yasm?还是masm、gas或其他?

前面三个是免费开源的汇编编译器,总体上来讲都使用Intel的语法。yasm是在nasm的基础上开发的,与nasm同宗。由于使用了相同的语法,因此nasm的代码可以直接用yasm来编译。

yasm虽然更新较慢,但对nasm一些不合理的地方进行了改良。从这个角度来看,yasm比nasm更优秀些,而nasm更新快,能支持更新的指令集。在Windows平台上,fasm是另一个不错的选择,平台支持比较好,可以直接用来开发Windows上的程序,语法也比较独特。在对Windows程序结构的支持上,fasm是3个免费的编译器里做得最好的。

masm是微软发布的汇编编译器,现在已经停止单独发布,被融合在Visual Studio产品中。gas是Linux平台上的免费开源汇编编译器,使用AT&T的汇编语法,使用起来比较麻烦。

由于本书的例子是在祼机上直接运行,因此笔者使用nasm,因为它的语法比较简洁,使用方法简单,更新速度非常快。不过如果要是用nasm来写Windows程序则是比较痛苦的,这方面的文档很少。

从nasm的官网可以下载最新的版本:,也可以浏览和下载其文档:。

Part 3: The Boot Loader

终于到了boot loader部分了……

Floppy and hard disks for PCs are divided into 512 byte regions called
sectors. A sector is the disk’s minimum transfer granularity: each
read or write operation must be one or more sectors in size and
aligned on a sector boundary. If the disk is bootable, the first
sector is called the boot sector, since this is where the boot loader
code resides. When the BIOS finds a bootable floppy or hard disk, it
loads the 512-byte boot sector into memory at physical addresses
0x7c00 through 0x7dff, and then uses a jmp instruction to set the
CS:IP to 0000:7c00, passing control to the boot loader. Like the BIOS
load address, these addresses are fairly arbitrary – but they are
fixed and standardized for PCs.

  1. First, the boot loader switches the processor from real mode to
    32-bit protected mode, because it is only in this mode that
    software can access all the memory above 1MB in the processor’s
    physical address space. Protected mode is described briefly in
    sections 1.2.7 and 1.2.8 of PC Assembly
    Language,
    and in great detail in the Intel architecture manuals. At this
    point you only have to understand that translation of segmented
    addresses (segment:offset pairs) into physical addresses happens
    differently in protected mode, and that after the transition
    offsets are 32 bits instead of 16.
  2. Second, the boot loader reads the kernel from the hard disk by
    directly accessing the IDE disk device registers via the x86’s
    special I/O instructions. If you would like to understand better
    what the particular I/O instructions here mean, check out the “IDE
    hard drive controller” section on the 6.828 reference
    page.

1.从BIOS跳转到boot loader第一条指令后, boot
loader首先把处理器从实模式切换到保护模式,
我们暂时只要知道保护模式下寻址方式不同于段基址*16 +
偏移地址的寻址方式, 并且偏移地址从16位变为32位。
保护模式下最大的特点就是虚拟内存, 通过虚拟内存,
不同进程间地址空间不能相互访问,并且给予进程独享32位地址空间的假象。想深入了解
virtual memory 可看
OSTEP
的12~24章。
2.boot loader 把内核的其余部分从IDE 磁盘
读入到内存中(通过I/O指令读写磁盘的寄存器), 具体细节可看
[OSTEP的36章]
(http://pages.cs.wisc.edu/~remzi/OSTEP/file-devices.pdf)。

这书写的实在是好, 我看完CSAPP再看的OSTEP,
基本把操作系统的原理弄的明明白白, 值得一直安利。

Exercise 3.
先在boot
loader第一条指令0x7c00处打个断点再c运行到这,后面再用si指令一步一步地运行。

图片 1

图片 2

类似于boot loader,
先关闭中断设置串操作方向,清零段寄存器,加载全局描述符, 最后通过ljmp
$0x8, $0x7c32, 设置CS:IP从而跳转到下面一条指令并进入保护模式,

图片 3

boot/boot.S对应的就是bootloader的代码

图片 4

所以movl $start, %esp设置内核的栈值为start,就是boot loader第一条指令

图片 5

call bootmain跳转到bootmain

图片 6

试着去si把每条汇编指令跟上面的C语言对应起来

反编译main.o方便我们对照:

图片 7

图片 8

回答一些问题:
Be able to answer the following questions:

At what point does the processor start executing 32-bit code? What
exactly causes the switch from 16- to 32-bit mode?

答:从(boot.S) ljmp指令开始执行32位代码,
把cr0的PE位置1引起了16位到32的转变。

图片 9

What is the last instruction of the boot loader executed, and what is
the first instruction of the kernel it just loaded?

在main.c 的 bootmain 的最后跳转到ELF文件的进入口 e_entry

图片 10

Where is the first instruction of the kernel?

反汇编kern可执行文件

图片 11

可以发现第一条指令的地址是0x0010000c,设置断点运行到这

图片 12

发现正是kern/entry.S的第一条指令

图片 13

How does the boot loader decide how many sectors it must read in order
to fetch the entire kernel from disk? Where does it find this
information?
从bootmain代码我们可以看出boot loader把kernel作为一个ELF文件读取·
, elf
header记录了文件信息。通过检查ELF头文件(第一个sector)的e_magic位检查是否有效的elf文件,通过ELFHDR->e_phnum知道kernel有多少段,通过循环把它们读到内存中load
address的地方。

图片 14

多说一句, elf也是linux的可执行文件的格式。
Exercise 4.
不多说,
需要掌握指针再继续下面的实验。把CSAPP有关部分看完和代码跑跑就大概掌握了。

elf可执行文件的格式:

An ELF binary starts with a fixed-length ELF header, followed by a variable-length program header listing each of the program sections to be loaded. The C definitions for these ELF headers are in inc/elf.h. The program sections we're interested in are:

.text: The program's executable instructions.
.rodata: Read-only data, such as ASCII string constants produced by the C compiler. (We will not bother setting up the hardware to prohibit writing, however.)
.data: The data section holds the program's initialized data, such as global variables declared with initializers like int x = 5;.

objdump查看elf文件的section(-h)的信息

图片 15

Take particular note of the "VMA" (or link address) and the
 "LMA" (or load address) of the .text section. The load 
address of a section is the memory address at which that 
section should be loaded into memory.

The link address of a section is the memory address from which the section expects 
to execute. The linker encodes the link address in the binary in various ways, 
such as when the code needs the address of a global variable, with the result 
that a binary usually won't work if it is executing from an address that it is 
not linked for. (It is possible to generate position-independent code that 
does not contain any such absolute addresses. This is used extensively by 
modern shared libraries, but it has performance and complexity costs, so 
we won't be using it in 6.828.)

load address v.s. link address:
load adress:一个 section 应该被加载到的内存地址
link address:一个section希望在这里执行的内存地址

Exercise 5. 修改 boot/Makefrag里的 boot loader 的link address
0x7c00,看看有什么事发生
当修改了boot loader的链接地址,这个指令就会出现错误。

不要忘了改回去再继续下面的实验。

跟boot loader不同的是,
kernel被加载到低地址段,但是它希望在高地址段执行,

图片 16

可以看到VMA和LMA相差了0xf0000000

Exercise 6.
Reset the machine (exit QEMU/GDB and start them again). Examine the 8
words of memory at 0x00100000 at the point the BIOS enters the boot
loader, and then again at the point the boot loader enters the kernel.
Why are they different? What is there at the second breakpoint? (You do
not really need to use QEMU to answer this question. Just think.)

图片 17

通过对比,我们发现进入boot
loader和进入kernel时0x100000处的数据发生了变化,
我们尝试把这些数据看作指令, 发现它们就是kernel entry处开始的指令!

图片 18

因为bootmain函数在最后会把内核的各个程序段送入到内存地址0x00100000处,所以这里现在存放的就是内核的某一个段的内容,由于程序入口地址是0x0010000C,正好位于这个段中,所以可这里面存放的是指令段也就是.text段的内容。
  
这里Lab 1的Part 2部分的所有实验就完成了。

机器语言

一条机器指令由相应的二进制数标识,直接能被机器识别。在汇编语言出现之前,使用机器指令编写程序是直接将二进制数输入计算机中。

C语言中的c=a+b在机器语言中应该怎样表达?

这是一个很麻烦的过程,a、b和c都是变量,在机器语言中应该怎样表达?C语言不能直接转换为机器语言,要先由C编译器译出相当的assembly,然后再由assembler生成机器指令,最终再由链接器将这些变量的地址定下来。

我们来看看怎样转化机器指令。首先用相应的汇编语言表达出来。

        mov eax,
[a]               ;
变量 a
的值放到
eax寄存器中

        add eax,
[b]                ;执行 a+b

        mov [c],
eax                ;放到 c中

在x86机器中,如果两个内存操作数要进行加法运算,不能直接相加,其中一方必须是寄存器,至少要将一个操作数放入寄存器中。这一表达已经是最简单形式了,实际上当然不止这么简单,还要配合程序的上下文结构。如果其中一个变量只是临时性的,C编译器可能会选择不放入内存中。那么这些变量是局部变量还是外部变量呢?编译器首先要决定变量的地址。

        mov eax,
[ebp-4]                ;变量 a是局部变量

        add eax,
[ebp-8]                ;执行 a+b,变量b也是局部变量

        mov [0x0000001c],
eax          ;放到
c中,变量c可能是外部变量

变量a和b是在stack上。在大多数的平台下,变量c会放入到.data节,可是在进行链接之前,c的地址可能只是一个偏移量,不是真正的地址,链接器将负责用变量c的真正地址来代替这个偏移值。

上面的汇编语言译成机器语言为

        8b 45
fc                 ;对应于  mov eax, [ebp-4]

        03 45
f8                 ; 对应于  add eax, [ebp-8]

        a3 1c 00 00
00         ; 对应于 
mov [0x0000001c], eax

x86机器是CISC(复杂指令集计算)体系,指令的长度是不固定的,比如上述前面两条指令是3字节,最后一条指令是5字节。

 x86机器指令长度最短1字节,最长15字节。

最后,假定.data节的基地址是0x00408000,那么变量c的地址就是0x00408000+0x1c =
0x0040801c,经过链接后,最后一条机器指令变成

        a3 1c 80 40
00         ; 原始汇编表达形式:  mov [c], eax

指令同样采用little-endian存储序列,从低到高依次存放a3 1c 80 40
00字节,其中1c 80 40
00是地址值0x0040801c的little-endian字节序排列。

Hello world

按照惯例,我们先看看“Hello,
World”程序的汇编版。

实验2-1:hello
world程序

下面的代码相当于C语言main()里的代码。

代码清单2-1(topic02ex2-1setup.asm):

main:                                                       ;这是模块代码的入口点。

       

        mov si,
caller_message

        call
puts                                                     ;打印信息

        mov si,
current_eip       

        mov di,
caller_address

current_eip:       

        call
get_hex_string                                      
;转换为 hex

        mov si,
caller_address                                        

        call puts

       

        mov si,
13                                                         
;打印回车

        call putc

        mov si,
10                                                         
;打印换行

        call putc

       

        call
say_hello                                            ;打印信息

       

        jmp $       

 

 

caller_message        db
‘Now: I am the caller, address is 0x’

caller_address        dq
0

 

hello_message         db 13,
10, ‘hello,world!’, 13,10

                          db
‘This is my first assembly program…’, 13, 10, 13, 10, 0

callee_message        db
“Now: I’m callee – say_hello(), address is 0x”

callee_address        dq
0

实际上这段汇编语言相当于下面的几条C语言语句。

int main()

{

        printf(“Now: I am the caller, address is 0x%x”,

                                                
get_hex_string(current_eip));

        printf(“n”);

 

        say_hell0();                /* 调用 say_hello() */

}

相比而言,汇编语言的代码量就大得多了。下面是say_hello()的汇编代码。

代码清单2-2(topic02ex2-1setup.asm):

;——————————————-

; say_hello()

;——————————————-

say_hello:

        mov si,
hello_message

        call
puts                                                                 

       

        mov si,
callee_message                                               

        call puts

       

        mov si,
say_hello                                                       

        mov di,
callee_address

        call
get_hex_string

       

        mov si,
callee_address

        call puts

        ret

这个
say_hello()也仅相当于以下几条C语句。

void say_hello()

{

        printf(“hello,worldnThis is my first assembly program…”);

        printf(“Now: I’m callee – say_hello(), address is 0x%x”,

                                          get_hex_string(say_hello));

}

代码清单2-1和2-2就组成了我们这个16位实模式下的汇编语言版本的hello world程序,它在VMware上的运行结果如下所示。

 

当然仅这两段汇编代码还远远不能达到上面的运行结果,这个例子中背后还有 boot.asm和lib16.asm的支持,boot.asm用来启动机器的MBR模块,lib16.asm则是16位实模式下的库(在lib目录下),提供类似于C库的功能。

main()的代码被加载到内存0x8000中,lib16.asm的代码被加载到
0x8a00中,作为一个共享库的形式存在。这个例子里的全部代码都在topic02ex2-1目录下,包括boot.asm源文件和setup.asm源文件,而lib16.asm则在x86sourcelib目录下。main()所在的模块是 setup.asm。

16位?32位?还是64位?

在机器启动时处理器工作于16位实模式。这个hello world程序工作于16位实模式下,在编写代码时,需要给nasm指示为16位的代码编译,在代码的开头使用bits 16指示字声明。

bits
32指示编译为32位代码,bits 64指示编译为64位代码。

 

本文节选自《x86x64体系探索及编程》

图片 19

 

电子工业出版社出版

邓志著