我本月开始尝试探索如何编写shellcode。我的搜索使我想学习汇编程序,因此我用NASM编写了一个简单的引导程序:
                bits       16               ; 16 bit real mode
                org        0x7C00           ; loader start in memory

start:          jmp        main             ; goto main

bgetkey:        mov        ax, 0            ; clear register a
                mov        ah, 0x10         ; 
                int        16h              ; interrupt bios keyboard
                ret                         ; return
                .buf       dw 0             ; buffer size one word

prints:         pusha                       ; 
.loop:          mov        ah, 0x0e         ; 
                mov        al, [si]         ; 
                cmp        al, 0            ; check for null terminator
                jz         print_end        ; stop printing
                mov        bh, 0x00         ; 
                mov        bl, 0x07         ; 
                int        0x10             ; interrupt bios tty
                inc        si               ; next character
                jmp        .loop            ; jump beginning
print_end:      popa                        ; 
                ret                         ; return

main:           mov        ax, 0x0000       ; clear register a
                mov        ds, ax           ; 
                mov        si, welcome      ; copy welcome string pointer
                call       prints           ; print string
newinput:       mov        bx, mem          ; set register b to memory start
                add        bx, word 2       ; increment by size of memory ptr
                mov        word [mem], bx   ; set pointer at first memory byte
type:           mov        si, qbuf         ; set byte buffer ptr for printing
                call       bgetkey          ; capture keyboard input
                mov        [qbuf], al       ; copy key byte to buffer
                call       prints           ; print the character
                mov        bx, [mem]        ; copy memory stop to b, decrement
                cmp        bx, stop - 1     ; check for overflow preserve null
                je         oom              ; halt - no more memory
                mov        byte [bx], al    ; copy keystroke to memory
                add        bx, byte 1       ; increment memory pointer
                mov        [mem], bx        ; restore memory pointer to memory
                cmp        byte [qbuf], 0x0D; check for carriage return
                jne        type             ; goto next key if not found
                mov        si, newline      ; copy pointer to line feed string
                call       prints           ; print the line feed

                mov        bx, mem + 2      ; restore start of valid memory
readmem:        cmp        byte [bx], 0x0D  ; check for carriage return
                je         readmemdone      ; if found begin another input line
                mov        cl, [bx]         ; register character byte
                mov        byte [qbuf], cl  ; copy byte to string buffer
                mov        si, qbuf         ; copy buffer ptr for printing
                call       prints           ; print the character
                inc        bx               ; increment memory pointer
                jmp        readmem          ; read another character byte
readmemdone:    mov        byte [qbuf], 0x0D; copy carriage return to buffer
                call       prints           ; print carriage return
                mov        si, newline      ; copy line feed string to buffer
                call       prints           ; print line feed
                jmp        newinput         ; ready new line

oom:            mov        si, outomem      ; copy out of memory message ptr
                call       prints           ; print message

halt:           mov        si, halting      ; copy halting mesage ptr
                call       prints           ; print final message
                hlt                         ; halt the cpu

                welcome db "boot", 0x0A, 0x0D, 0x00
                newline db 0x0A, 0x00
                outomem db 0x0A, 0x0D, "out of memory", 0x0A, 0x0D, 0x00
                halting db 0x0A, 0x0D, "halting", 0x00
                qbuf       dw 0, 0
                mem        db 0

times 0200h - 2 - ($ - $$)db 0
                stop       dw 0xAA55

我使用脚本来编译和运行该程序。每次都会对所有文件进行后备备份:
nasm -o boot.bin -f bin boot.asm && \
tar -zcvf ~/os-$(date +%Y%m%d%H%M%S).tar.gz ../os && \
qemu boot.bin

如果我键入:

测试123
hello loader
溢出测试[按.直到停止]

我得到以下正确的输出:

在较早的时间点,我认为我永远不需要学习低级语言。我错了。一旦我了解了它们的作用,空白注释就是占位符。我主要是要求对评论的准确性提出反馈,其次是要寻求大小的效率以及整体流程和布局的正确性。

#1 楼

有关代码原始机制的大量评论。没有太多的设计审查。因此,这里只是一些设计技巧。

使用BPB,因为其他人会这样做。

程序可以做到这一点。首先也是为了跳过嵌入式BIOS参数块。您可能不认为您的卷中需要BPB。您几乎可以肯定会。其他人则认为,多年来,后来在做诸如没有BPB的事情时被咬伤,并使用要求各种分区在VBR中具有BPB的操作系统中的工具。它在OS / 2引导管理器中咬住了IBM。

第二个是,甚至有一些工具(损坏的工具,幸运的是不再广泛使用),可以使VBR中的第一条指令无效,期望它是jmp

您的代码是否会在1979年生产的原始8088上运行? >

但不是出于所述原因。它们阻止了仅在cli的一半已加载的窗口中发生中断,因此堆栈指针无效。即使没有伴随这些其他段加载的偏移量,也没有必要将这808x智慧错误地扩展到该答案中的其他段寄存器。实际上,中断处理程序不希望其他寄存器具有任何特定值,并且您无需临时禁用中断即可对其进行操作。 8086“ far”内存模型DOS程序始终可以在不屏蔽中断的情况下操作stiss:sp。而且重要性被夸大了。自8088年以来,一个ds隐含地导致中断(甚至es无法屏蔽的中断)延迟一条指令,因此一个人可以在它之后立即插入mov ss,而无需显式cli,两条指令将隐含中断安全/ mov sp对。 (早期的8088s有一个错误,但是8088参考手册将其记录为这种行为。今天,它仍然存在于Intel参考手册中。)从80386开始,我们有了cli指令,该指令将寄存器对装入一条指令。 />
有趣的历史观点

在1987年的PC Magazine中,罗伯特·L·胡默尔(Robert L. Hummel)称8088错误为“应该引起的严重错误”,并解释了(当时已经是民俗的)解决方法stilss esp配对中的一个。实际上,英特尔已在1981年修复了该错误。80386于1985年问世。三十年后,人们仍在遵循民间规则来编码这些东西,甚至变得失真了。

正确退出。

start:          jmp        main             ; goto main


恭喜!您刚刚执行了一个提示字符串作为代码。

IBM PC兼容固件提供了两个软件中断,用于正确地将引导程序“退出”到固件。 BIOS引导规范说明了它们的用法。

进一步阅读


Jonathan de Boyne Pollard(2006)PC / AT引导过程。常见问题解答。
乔纳森·德·博因·波拉德(Jonathan de Boyne Pollard,2011年)。一个PC / AT风格的MBR引导程序,用于EFI分区光盘。软件。
乔纳森·德博因·波拉德(2006)。有关BIOS参数块的所有信息。
Jonathan de Boyne Pollard(2006)卷启动块中OEM名称字段的含义和使用。
Will Fastie(1983年9月/十月)。 “跟踪8088中的错误”。 PC技术笔记本电脑4。 106
Sergei Kiselev(2011)“堆栈更改竞赛条件”。历史记录。
Robert L. Hummel(1987-12-08)。 “ PC辅导员:8088芯片有缺陷”。 PC杂志。第6卷第21期.ISSN 0888-8507。齐夫-戴维斯。 p.492。
康柏计算机公司;凤凰科技有限公司;英特尔公司。 BIOS引导规范1996-01-11。


评论


\ $ \ begingroup \ $
除了BIOS引导规范没有描述如何从引导加载程序退回到固件之外,都是好的建议。相反,它描述的是BIOS的功能,而不是引导程序需要执行的操作。我还将提供此真实的引导程序源代码作为灵感。
\ $ \ endgroup \ $
–爱德华
15年6月21日在14:58

\ $ \ begingroup \ $
您尚未正确阅读。仔细阅读所有内容,并特别注意附录D。
\ $ \ endgroup \ $
– JdeBP
2015年6月21日15:06

\ $ \ begingroup \ $
我实际上对它很熟悉-撰写本文时,我在Compaq从事BIOS开发。它表示如果操作系统不存在或无法加载,则应执行int 18h。在此引导加载程序中,这两个条件都不成立,因此建议不适用。
\ $ \ endgroup \ $
–爱德华
15年6月21日在15:12

#2 楼

您与清除寄存器的方式非常不一致:

bgetkey:        mov        ax, 0            ; clear register a




main:           mov        ax, 0x0000       ; clear register a


我建议xor ing

示例:

xor ax, ax



@icktoofay在本节中建议:

bgetkey:        mov        ax, 0            ; clear register a
                mov        ah, 0x10         ; 
                int        16h              ; interrupt bios keyboard
                ret  


不必清除所有ax并单独更改ax,只需执行以下操作:在一条指令中更改ax的高字节和低字节。


我发现在引导加载程序中很常见,因为人们发现它采用了“简便的方法”。相反,人们经常手动设置以下段:

请参阅bios内存映射(堆栈所在的位置):

mov ax, 0x1000


请注意,org 0x7C00cli非常重要。当处理段时,中断可能会“意外”触发,这可能会使您的程序混乱。 sti将禁用中断,而cli将重新启用中断。


在这样的某些行上:认为sti是必要的,因为byte的大小已经是一个字节了,尽管我尚未检验该理论。不知道它们是什么,而其他字符可能需要一点时间来处理。 />
例如,在
cli
mov ax,07C0h
mov ds,ax
mov gs,ax
mov fs,ax
mov es,ax
例程中,即使al不是,每次迭代都使用0x0A恢复0x0D。在例行程序中完全没有接触过。

我建议放下

mov ax,07E0h
mov ss,ax
mov bp,ax

mov sp,0xff
sti


%define标签之前,因此您不必要地更新prints。 >我将继续浏览代码,并添加我认为必要的更多改进。

评论


\ $ \ begingroup \ $
重新。建议用mov al,0替换mov ax,0:是的,但是我还要走得更远,如果您要更改高字节和低字节,请同时用mov ax更改它们, 0x1000。
\ $ \ endgroup \ $
–icktoofay
15年6月21日在2:24

\ $ \ begingroup \ $
@icktoofay啊,是的!这是一个好主意。您介意我修改我的帖子以适应您的建议吗?
\ $ \ endgroup \ $
– SirPython
2015年6月21日,下午2:31

\ $ \ begingroup \ $
请一定做!
\ $ \ endgroup \ $
–icktoofay
2015年6月21日,下午2:32

\ $ \ begingroup \ $
我看到的在该代码中禁用中断的唯一需求是MOV无法原子更新SS和SP。两次更新之间的中断将使用不一致的堆栈指针。无需禁用中断就可以安全地完成其他段寄存器的更新。
\ $ \ endgroup \ $
–卡巴斯德
15年6月21日在16:50

\ $ \ begingroup \ $
@SirPython我实际上已经忘记了它是0000:7C00 ...已经很久了。但是,我的主要观点是,在OP希望使用的任何硬件(包括VM)上,CS为0000,而不是07C0。自8086年代初以来,它就没有被破坏过,因此,再也没有必要继续进行该代码了。这是一个剩余的“民俗”,人们在它的到期日期后很长时间一直在疏dr。如果要重新放置加载器,则最终需要跳远,但这不是因为寄存器不正确。
\ $ \ endgroup \ $
– phyrfox
15年6月23日在18:26

#3 楼

我看到了许多可以帮助您改进代码的方法。常量,例如2、0x0e,0x10等。通常最好避免这种情况,并为此类常量赋予有意义的名称。这样,如果需要更改任何内容,则无需遍历代码中的所有“ 7”实例,然后尝试确定此特定的0x07是否与所需的更改相关,或者是否与其他更改有关具有相同值的常数。使用NASM,可以使用%define指令:

%define KBDINT 16h


然后在代码中:

int KBDINT


使用XOR清除寄存器

x86汇编语言中清除寄存器的惯用方式是使用xor

xor ax,ax       ; ax = 0


此指令编码是比mov ax,0000h短。

使用注释指示寄存器使用情况

跟踪寄存器使用情况是汇编语言程序员最重要的任务之一。跟踪此内容的一种有用技术是注释的使用。例如,代替这个:

bgetkey:        mov        ax, 0            ; clear register a
                mov        ah, 0x10         ; 
                int        KBDINT           ; interrupt bios keyboard
                ret                         ; return


编写这个: br />
尽管NASM中的宏支持不是很好,但它确实存在并且可以用来简化您的代码。例如,上面的例程仅被访问一次。上面的简化版本只有3条指令,但是只有2条指令,如果将call内嵌在代码中,则可以省去。我会这样写:

;****************************************************************************
;
; bgetkey - use BIOS call to get a keystroke; blocks until key available
;
; INPUT:        none
; OUTPUT:       ah = BIOS scan code, al = ASCII char
; DESTROYED:    none
;****************************************************************************
bgetkey:       
     mov        ah, 0x10         ; 
     int        KBDINT           ; interrupt bios keyboard
     ret                         ; return


然后在这样的代码中使用:

%macro BIOSWAITKEY 0
        mov        ah, 0x10         ; 
        int        KBDINT           ; interrupt bios keyboard
%endmacro


显式设置段寄存器

调用装载程序的BIOS应该将CSDS都设置为0,但是很遗憾,不能保证。一些旧的BIOS会调用7C0:0而不是0:7C00,因此大多数引导加载程序代码都会明确设置段寄存器。您的代码仅设置DS寄存器,而不设置SS。对于健壮的引导程序,请将段寄存器显式设置为0或等于CS

仔细考虑堆栈使用情况

prints例程当前先推送然后弹出所有寄存器。这个例程对速度没有严格要求,但是养成仔细考虑堆栈使用情况的习惯很有用。在这种情况下,我可能只会保存AXBX或仅保存BX。该代码实际上并不需要保留SI的值,也不一定要保留AX的值,只需对调用代码进行一些小的改动。维护代码与标签在同一行上的代码很麻烦。更好的做法是让每个标签自己排成一行。这使得维护代码更加容易。节省周期和时间。在这段代码中,我们有:

type:   
        mov        si, qbuf         ; set byte buffer ptr for printing
        BIOSWAITKEY
        mov        [qbuf], al       ; copy key byte to buffer


这意味着循环末尾的无条件jmp总是被执行。相反,最好将代码重组为在循环内只有一个条件分支。

AX寄存器设置为靠近INT指令

为了使其他程序员更容易理解您的代码,最好在AX指令调用BIOS或操作系统功能之前设置AH(或INT寄存器)。这样,两个最重要的信息,即“哪个中断”和“哪个服务”彼此靠近,因此很容易查找它们。更好的是,使用命名常量并将它们彼此靠近。

消除未使用的变量

永远不要使用.buf中的bgetkey区域,应该将其删除。

优先选择运行时数学的汇编时间

newinput标签以以下三行开头:

prints:         push       ax               ; modified per previous point
                push       bx               ;
.loop:          mov        ah, 0x0e         ; 
                mov        al, [si]         ; 
                cmp        al, 0            ; check for null terminator
                jz         print_end        ; stop printing
                mov        bh, 0x00         ; 
                mov        bl, 0x07         ; 
                int        0x10             ; interrupt bios tty
                inc        si               ; next character
                jmp        .loop            ; jump beginning
print_end:      pop        bx               ; 
                pop        ax               ;
                ret                         ; return


更好而是让汇编程序来进行计算:也就是说,只需使用mem来存储指针并将所有bx用作缓冲区。

在适当的地方简化调用机制

mem结构似乎仅是为了打印单个字符输出而设置的。在代码中,将qbuf的第一个字节设置为一个值,然后将qbuf指向si,然后调用qbuf例程。最好通过创建一个例程来简单直接地打印单个字符来简化。实际上,这是BIOS视频TTY输出例程(您正在使用的)实际上已经在执行的操作,因此请为该函数创建一个函数:

prints:         
        push       ax               ; 
        push       bx               ;
        jmp        .begin           ; skip over loop first iteration
.loop:          
        mov        bx, PAGE0WHTBLK  ; page 0, white on black
        mov        ah, TTYOUT       ; 
        int        VIDINT           ; interrupt bios tty
        inc        si               ; next character
.begin:
        mov        al, [si]         ; 
        cmp        al, 0            ; check for null terminator
        jnz        .loop            ; keep printing
        pop        bx               ; 
        pop        ax               ;
        ret                         ; return
现在,宏对于使用该例程非常有用:

    mov        bx, mem          ; set register b to memory start
    add        bx, word 2       ; increment by size of memory ptr
    mov        word [mem], bx   ; set pointer at first memory byte


现在可以像以下任何一种方式使用它:
命名重要的存储位置

一个非常重要但未命名的存储位置是prints区域的末尾。我将修改现有代码以使其看起来像这样:

    mov        bx, mem+2        ; point to available space
    mov        word [mem], bx   ; save pointer to available space


充分利用每个字节

,特别是在引导加载程序中,每个字节都很重要,利用每个可用字节很有用。例如,在您的代码开始之后,不再需要“欢迎”消息。您可以将该消息覆盖在mem缓冲区中,以允许它被用户输入覆盖。同样,从技术上讲,一旦将扇区读入内存,签名mem字节也可能会被覆盖。同样,错误消息字符串也可以合并以节省一些字节。但是典型的x86汇编语言代码在第1列中带有标签格式,并在第9列中缩进代码(即,一个传统制表位的大小)。之后,注释通常会在制表位的多个倍数处对齐(如您所愿)。如果将0xAA55指向内存缓冲区,则可以避免保存di寄存器。

注释应该说明原因,而不是原因

注释通常应该说明您为什么做自己的工作,而不是简单地重复说明。因此,这不是一个好评论:

;****************************************************************************
;
; printch: prints a single character to screen
;
; INPUT:        al = character to print
; OUTPUT:       none
; DESTROYED:    none
;****************************************************************************
printch:
            push       ax               ; 
            push       bx               ;
            mov        bx, PAGE0WHTBLK  ; page 0, white on black
            mov        ah, TTYOUT       ; 
            int        VIDINT           ; interrupt bios tty
            pop        bx               ; 
            pop        ax               ;
            ret                         ; return


这是一个更好的评论:

%macro PRINTCHAR 1
    %ifnidni %1,al
        mov al, %1
    %endif
        call printch
%endmacro


将所有内容放在一起

应用所有这些建议将产生一个易于维护,易于阅读,更小且结构更好的程序: >
PRINTCHAR al
PRINTCHAR [bx]
PRINTCHAR CR


使用真实版本控制,而不仅仅是文件副本

快速而实验性地更改源文件时,拥有备份文件固然重要,但您可以考虑为此使用更合适的机制。我建议您可能要使用bx而不是创建多个gzip文件。这样,即使您不打算共享代码,也更容易记录更改原因。

#4 楼

我回覆了SirPython的大部分建议(可能的建议段设置代码除外,我更喜欢设置所有段寄存器为零的平面内存空间),但还有其他一些我可能要更改的地方。我应该注意,其中一些可能更多是关于品味的问题。

十六进制常量

您与十六进制常量的表示法不一致。您在某些地方使用0xABCD语法,而在其他地方使用0ABCDh语法。我更喜欢0ABCDh语法,但这只是我一个。

(本质上)使用.data,其中.bss足够了

将行缓冲区放入分配给引导加载程序的空间中。在编写代码时,可用空间将减少。当然,如果您需要将某些空间初始化为某些特定的初始值,那么这很方便,因此应该将其保留在其中,但是如果您不需要将其初始化为特别的任何东西,则无需将其填充到那些初始值中512字节您几乎可以使用整个地址空间。 (从500h开始,直到您自己敲击引导程序代码,然后直到……gee,还有其他事情。但是您还有很多空间。)但是请记住,如果您希望它为零,已初始化,现在您必须自己对其进行零初始化。

字符串指令

我看到很多代码访问缓冲区,对其进行处理,然后递增该指针。帮自己一个忙,阅读x86体系结构提供给您的字符串指令:尤其是lodsbstosb可能会成为您最好的朋友。

inc

几乎不需要add reg, 1。节省空间并使用inc reg。 (使用add时,您需要空间来存储1。对于inc则不是。)还有dec会递减,如果以后需要的话。同样,考虑使用test reg, reg而不是cmp reg, 0。同样,这避免了立即存储0的需要。 (这也是为什么当xor reg, reg大于一个字节时,mov reg, 0优于reg的原因。)在这里,是样式问题。当我使用局部点标签时,通常会缩进它们以表示某种结构,例如:

标签,所以我会像处理其他任何标签一样冲洗掉剩下的标签。删除例如db上的操作数大小提示。在某些地方您无法删除它们(最常见的情况是在使用带有内存操作数和立即数操作的指令时使用),但是在其他情况下,我认为它很混乱。

选择汇编器

这完全是个人喜好,但是我使用了一段时间的NASM,直到遇到了我认为是一个非常奇怪的错误,该错误开始在16和20 32位边界。当我遇到该问题时,我切换到FASM(语法非常相似)。这解决了我的问题,从那以后,我变得越来越喜欢它,只是因为它确实造成了一些语法上的差异。您可能也想尝试一下。


1 FASM所做的NASM没有做的一件事是它会记住您声明数据定义是哪种类型。因此,例如:

read_line:
  .next_character:
  .done:


...将起作用; dw将是一个字节-mov byte [bx], al,因为mov被定义为字节。 (当然,可以使用指定的显式操作数大小来覆盖。)FASM的宏样式我也认为比NASM更好,FASM的mov非常适合在程序外部布局内存(如行缓冲区)。

评论


\ $ \ begingroup \ $
认识lodsb和stosb(以及scasb)和朋友是件好事,但也值得一提的是,它们不一定比相应的等效说明要快。在上下文中进行测试是了解的最佳方法。
\ $ \ endgroup \ $
–爱德华
15年6月22日在23:50

\ $ \ begingroup \ $
@Edward:我不建议您考虑速度,而建议考虑代码大小,因为引导加载程序中的空间非常宝贵。另外,它们非常方便。
\ $ \ endgroup \ $
–icktoofay
2015年6月25日在1:39

#5 楼

SyrPython和icktoofay的所有内容,再加上...

四个指令:
mov        ah, 0x0e         ; 
mov        al, [si]         ; 
cmp        al, 0            ; check for null terminator
jz         print_end        ; stop printing



如果我理解正确(那时候我使用了另一个汇编程序。我想是Borland) mem在bx中的偏移量。那为什么不这样做

mov        ax,0x0e00        ; clears al
or         al,[si]          ; sets Z accordingly (if [si] is 0)
jz         print_end        ; stop printing


就像您在其他地方一样?如果您使用建议的lodsb,则不会使用,可能会或可能不会与rep一起使用,取决于您的未来方向)

您有8个寄存器Ax Cx Dx Bx SP BP SI DI SP,您通常不会玩
SI和DI通常用于偏移量(将BX用于什么)
DX用于其他数据
BP主要用于堆栈框架,而您并不在乎,另一个免费的堆栈框架。 ,reg,这的确是一种首选方法,它唯一的缺点是会影响标志,因此在对代码进行流水线化(将操作按并行执行的顺序放置)时,不能很好地使用它> ie:

newinput:       mov        bx, mem          ; set register b to memory start
                add        bx, word 2       ; increment by size of memory ptr


cmp将在管道U中执行,而mov在管​​道V中执行,因为它不影响cmp操作。

但这可能对装载程序来说有点过头了。

评论


\ $ \ begingroup \ $
在这种情况下,我同意的U / V位居首位,但是当他们尝试优化视频硬件时,这些信息肯定很不错。
\ $ \ endgroup \ $
– phyrfox
15年6月22日在19:43