assembly 如何编译和运行汇编代码?

eqfvzcg8  于 2022-11-13  发布在  其他
关注(0)|答案(1)|浏览(188)

我看到汇编代码应该在汇编中创建一个 Boot 扇区。代码是:

jmp $
times 510 - ($ - $$) db 0
db 0x55, 0xaa

一切都很好,但是第一行被认为会创建一个无限循环。我来自一个更高级别的抽象编程,所以在我的脑海中,这意味着这行将永远执行(因为它会不断地跳到内存中的当前地址),其余的代码是如何执行的呢?是在编译过程中执行的吗?另外,通常x86汇编代码包含一个“开始”部分。2为什么这段代码可以工作?

njthzxwz

njthzxwz1#

汇编程序只是一个工具,它在一个文件中获取字节,在另一个文件中生成不同的字节。就像C编译器或字处理器(字处理器从用户那里获取输入,将其转换为文件中的字节和屏幕上的像素)。等等,等等。
汇编语言是特定于工具的,而不是特定于目标的,x86尤其麻烦,因为不兼容的汇编语言的列表无法计数(这不是Intel与at&t的语法问题,有无数不兼容的Intel x86汇编器)。
你不编译汇编语言(嗯,有些工具链你使用编译器来做汇编,sad,有些人故意使用编译器,这样他们本质上从同一个工具链中获得另一种汇编语言(使用gcc而不是as为同一个目标提供了不同的、不兼容的汇编语言))你汇编它。指令只是语言的一部分,指令,如何创建注解和标签等也是其中的一部分。
旧的汇编器基本上可以用最终的二进制代码结束,其中包含.org等指令,这样就可以把它用作链接器。汇编程序和链接程序。您创建对象(文件),然后将它们链接在一起。试想一下,如果像C这样的高级语言,你将有多个源文件(如果你选择的话),一个文件可以调用另一个文件中的函数,每个C文件都将成为它自己的对象,链接器不仅将所有对象链接在一起,定义它们在内存中的物理位置,而且将外部引用链接在一起,这样它们就可以调用彼此的函数,或者访问全局变量。
(Some像NASM do 这样的现代汇编器仍然支持生成平面二进制代码,填充符号地址本身而不需要链接器。这就是您问题中的NASM源代码通常是如何构建到一个512字节的传统BIOS MBR Boot 扇区中的,没有元数据,使用nasm -f bin foo.asm。但是GNU汇编器GAS不支持这样做,而这个答案只考虑了GNU工具链是如何完成的。)
所以我会努力

jmp $

即使我很确定它与我将要使用的汇编程序不兼容(来自binutils的gnu汇编程序)。

as so.s
so.s: Assembler messages:
so.s:1: Error: missing or invalid immediate expression `'

所以相反

jmp .

as so.s -o so.o
objdump -d so.o

so.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <.text>:
   0:   eb fe                   jmp    0x0

更好的是,我可以构建它,并看到汇编程序生成的机器代码。汇编程序的部分工作是将人类可读的jmp转换为处理器可以实际执行的字节/位(0xEB,0xFE)。
我对我的x86非常生疏,在这个级别上使用它没有太大价值,但0xFE是一个-2,它是一个2字节指令,所以看起来像一个偏移量,所以0xEB自然是8位x86指令,0xFE是偏移量。我们可以测试这个

here:
jmp here
jmp here
jmp here
jmp here
jmp here

我讨厌x86工具,等等,让我们回到这个问题上。
链接,start或_start是什么意思。至少对于gnu工具链来说,这是不需要的,也许其他的工具链,你必须写你的代码到工具链。如果你坚持使用高级语言和已经构建的工具,甚至gnu,血淋淋的细节已经为你做了。高级语言需要引导....在asm或一些低级语言中,先有鸡还是先有蛋的问题如果你试图用同一种语言引导,有些人尝试了,最终失败了。
注意在gnu汇编器中,并理解gnu支持许多“目标”(x86,arm,mips等),并不一定是通过设计,因为每个目标可能是由不同的人或作者团队创建的,但更多的是通过借用现有的目标代码将其转化为一些新的目标代码,如jmp。点表示此地址,它是这里的快捷方式:jmp在这里,而不需要输入那么多的文本。你可以用这些gnu汇编语言中的一些来做jmp .+2。
如果我这么做

as so.s -o so.o
ld so.o -o so.elf
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
objdump -d so.elf

so.elf:     file format elf64-x86-64

Disassembly of section .text:

0000000000401000 <__bss_start-0x1000>:
  401000:   eb fe                   jmp    401000 <__bss_start-0x1000>

因此,我们将其链接为最终二进制文件,并且加载/入口点为0x 401000。但是,我们没有为链接器指定任何内存地址,它如何确定这是我们需要代码的位置?因为工具链是为我的操作系统构建的(哦,是的,假设没有两个操作系统支持相同的二进制文件格式,也没有关于操作系统如何加载所述二进制格式的文件的相同规则,以及特定于每个操作系统的系统调用等等),并且它是用C库和用于该库的一些引导代码以及用于该目标的与用于该库的引导代码相结合的链接器脚本来构建的......有一个默认值。并且该默认链接器脚本,使用用于GNU链接器的链接器脚本语言,ld(假定没有两个工具链使用相同的链接器脚本语言),包含

ENTRY(_start)

它告诉链接器在二进制文件中标记label _start所在的入口点。它不一定是程序中的第一条指令。它几乎可以是任何地方,但要遵循操作系统加载器的规则。由于我没有指定它,所以选择了默认的链接器脚本。
现在就算我:

ld -Ttext=0x1000 so.o -o so.elf
ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000

objdump -d so.elf

so.elf:     file format elf64-x86-64

Disassembly of section .text:

0000000000001000 <__bss_start-0x1000>:
    1000:   eb fe                   jmp    1000 <__bss_start-0x1000>

这实际上是超级痛苦和丑陋的,因为它是采取别人的链接器脚本和黑客的一部分,但留给我们的其他部分:

jmp .
.data
.byte 0x55

ld -Ttext=0x1000 so.o -o so.elf

Disassembly of section .text:

0000000000001000 <.text>:
    1000:   eb fe                   jmp    1000 <__bss_start-0x1001>

Disassembly of section .data:

0000000000002000 <__bss_start-0x1>:
    2000:   55                      push   %rbp

所以如果我制作自己的链接器脚本

MEMORY
{
    one : ORIGIN = 0x00000000, LENGTH = 0x1000
    two : ORIGIN = 0x80000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > one
    .bss    : { *(.bss*)    } > two
}

并与之相连

as so.s -o so.o
ld  -Tso.ld so.o -o so.elf
objdump -d so.elf

so.elf:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <.text>:
   0:   eb fe                   jmp    0x0

它不再抱怨_start。因为我的没有ENTRY(_start)
但如果我:

ENTRY(banana)
MEMORY
{
    one : ORIGIN = 0x00000000, LENGTH = 0x1000
    two : ORIGIN = 0x80000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > one
    .bss    : { *(.bss*)    } > two
}

ld: warning: cannot find entry symbol banana; defaulting to 0000000000000000

所以很容易安装GNU Binutils工具,你可以看到使用它们是多么容易...
回到另一件事上

here:
jmp here
jmp here
jmp here
jmp here
jmp here
jmp here

反汇编可变长度指令集是相当痛苦的,甚至gnu的斗争,但有时你需要链接,特别是当看跳转,分支,调用等...

MEMORY
{
    one : ORIGIN = 0x00001000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > one
}

objdump -已删除

so.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <here>:
   0:   eb fe                   jmp    0 <here>
   2:   eb fc                   jmp    0 <here>
   4:   eb fa                   jmp    0 <here>
   6:   eb f8                   jmp    0 <here>
   8:   eb f6                   jmp    0 <here>
   a:   eb f4                   jmp    0 <here>

objdump -d so.elf

so.elf:     file format elf64-x86-64

Disassembly of section .text:

0000000000001000 <here>:
    1000:   eb fe                   jmp    1000 <here>
    1002:   eb fc                   jmp    1000 <here>
    1004:   eb fa                   jmp    1000 <here>
    1006:   eb f8                   jmp    1000 <here>
    1008:   eb f6                   jmp    1000 <here>
    100a:   eb f4                   jmp    1000 <here>

同样的机器码,像x86这样的汇编语言,如果有过载的助记符,可能会有近跳转和远跳转。
所以

.globl one
one:
jmp two

x.s

.globl two
two:
jmp one

so.ld

MEMORY
{
    one : ORIGIN = 0x00001000, LENGTH = 0x1000
    two : ORIGIN = 0x00002000, LENGTH = 0x1000
}
SECTIONS
{
    .one   : { so.o(.text)   } > one
    .two   : { x.o(.text)   } > two
}

as so.s -o so.o
as x.s -o x.o
ld -Tso.ld -o so.elf

objdump -d  so.o

so.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <one>:
   0:   e9 00 00 00 00          jmpq   5 <one+0x5>

objdump -d  x.o

x.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <two>:
   0:   e9 00 00 00 00          jmpq   5 <two+0x5>

objdump -d so.elf

so.elf:     file format elf64-x86-64

Disassembly of section .one:

0000000000001000 <one>:
    1000:   e9 fb 0f 00 00          jmpq   2000 <two>

Disassembly of section .two:

0000000000002000 <two>:
    2000:   e9 fb ef ff ff          jmpq   1000 <one>


在对象级别(So.o,x.o)汇编器不知道这些标签在哪里,So.s不具有两个标签,因此汇编器假定它是外部的,并且必须假定距离是远跳跃,并且照此编码(例如,如果它要产生EB 00,并且一旦链接标签就太远,现在代码有麻烦并且可能不可用,所以工具链已经设计了规则来解决这些问题,就像我们在上面jmp这里看到的那样。很明显,这是一个pc相对偏移量,而不是一个绝对地址。同样,每个指令都是两个字节,沿着直线向下的偏移量是2。如果在该代码中混合了到外部的长跳转,但汇编程序将其编码为短跳转,则链接器在插入三个字节时会尝试解析它并且改变操作码,则现在将后面的JMP here指令全部处理,因为它们是完整的机器码。
因此,无论如何,汇编器都会为偏移量填充零,然后链接器在执行任务时用真实的偏移量替换这些零。有时候,你会看到汇编器编码了一个到self的跳转(jmp .,here:jmp here),然后链接器将其修补。

b .
b two

00000000 <.text>:
   0:   eafffffe    b   0 <.text>
   4:   eafffffe    b   0 <two>

在对象层次上,而b 0对于第二个真正是b 4的是错误的...但因为这是对象才关心的。它是程序的一小部分而不是完整的程序。
现在,汇编语言、C语言等等。编译语言和汇编指令最终都是机器码。处理器不能直接执行机器码,也不能直接执行C代码,必须将其转换成它可以执行的东西。你能把这些语言转换成解释语言吗?当然,但通常不能。

0000000000001000 <one>:
    1000:   e9 fb 0f 00 00          jmpq   2000 <two>

Disassembly of section .two:

0000000000002000 <two>:
    2000:   e9 fb ef ff ff          jmpq   1000 <one>

所以在一天结束的时候,(64位等等)和任何处理器,处理器是非常非常非常非常愚蠢,他们真的只做他们被告知的。程序员是完全控制处理器崩溃与否,非常类似于火车,如果你不连接轨道线性和开关设置采取不同的轨道集,等等,火车就会坠毁。作为一个程序员,你必须提供一个处理器将遵循的指令顺序路径,这就是它所知道的全部,如果你搞砸了处理器可能会偶然发现看起来像代码的字节或者也许你运气好,火车在空中跳跃,只是碰巧降落在某个地方的某组轨道上,没有坠毁,但现在火车不在你想要的地方,所以这是一个更难调试的故障。
e9被读取,也就是说接下来的四个字节,小端字节序是一个偏移量。现在,从我们作为程序员开始的地方,我们认为已经消耗了5个字节

one: jmp two

存储器
并将“1”视为该指令的“地址”,但(伪)程序计数器在执行pc相对跳转之前有5个字节,因此指令中编码的偏移量与我们程序员所考虑的地址1相差5个字节。如果您允许,这些工具会为您执行此操作。
所以现在哑处理器接受指令并执行它,如果我把它链接到操作系统上,这将是一个无限循环。jmp到这个地址在那个地址跳转到这个地址在那个地址跳转到这个地址。
j.甚至更简单。

0:   eb fe                   jmp    0x0

用eb指令开始执行代码,这两个字节是整个指令,它是说向后跳两个字节。向后跳两个字节(哑)处理器找到一个eb和fe,它告诉它向后跳两个字节,它找到一个eb fe,然后......永远或直到被中断。
为x86选择另一个具有不同语法的汇编程序,也许$意味着在这个(标签一样)地址这里jmp这里。

And since I do not mess with x86, I would not be surprised if that jmp is not actually executed and the thing you are looking at is a construct defined by the bios as a way to mark something related to booting. bios (x86) is a whole other very long book or set of books, how an x86 boots now and historically, etc. If my guess is right then someone probably said, hey let's put a jump to self up front just in case someone tries to execute this. I could be very wrong on that, your questions were related to tools and execution and not about the boot sector itself. Which while it has the name boot in it, it was designed as a thing the bios used to get that media started. booting is often a series of steps from how the processor finds its first instruction on an often very limited single or set of media it can support, and then that code gets more of the processor or peripherals up to find other media and so on (bios on flash to boot sector on hard drive to file system to load a bootloader that then maybe loads kernel that has its own drivers for the peripherals and finds a file system and so on). And you are looking at but one step in the ladder.
Oh yeah....and "how does it get run" well barring the steps in the ladder above.
Normal (compiled/assembled/linked) programs are put into a binary format that is supported by the operating system (.exe, .coff, .elf, etc). That file format has to conform to rules for that operating system. Then when on a command line or point and click you try to run it the operating system has code that these days sets up a virtual environment/address space, that protects you from others and others from you, then loads the fractions of that file that are actually code and data into that virtual address space, then switches from superuser level for the processor into a user/application mode and jumps to the entry point defined by the binary.
Toolchains like gnu and llvm and others can be used to generate programs that do not conform to the specific host operating system. The tools are somewhat generic. You can make C programs without a main() and without support for the C library, it is somewhat trivial. You could for example create some x86 code that if you knew how and where to put it on the flash that the processor uses to boot on your motherboard, it would run that code instead of the bios. BUT you are now conforming to rules of a different environment, and many of the components of the binary file format that is the default output for that toolchain may not be used. If you are creating the first instructions the processor boots, then there is no file system there is no operating system (sometimes no memory) there is nothing that can parse an exe or elf file, it is pure data and machine code. You need some tool(s) that take the elf file for example and with some hardware or probes or magic box you put a chip in extract the bytes and program them into that chip. Or some tool takes the elf file, makes another file format that the program the chip hardware knows how to use, per the rules of the program the chip hardware.
x86 is pretty much the worst first place you want to try this, "but I have one" is the worst excuse, you have four to 100 times as many arm processors as you have x86 processors. But "I have one" is not a good reason. If you want to work at this level. Start with a better and/or simpler instruction set/processor and a simulator/emulator. You will not brick anything, you will not let smoke out of anything, and your odds of success are significantly higher because you have better visibility/debug into what is going on. You can't see/debug that bios trying to read and use the boot sector on some media. (unless you have an emulator for that or special tools and knowledge for that specific motherboard).

相关问题