bitravel 发表于 2019-9-19 15:53:37

用机器码写开机能运行的hello world!

本帖最后由 bitravel 于 2019-9-19 16:07 编辑

转载于:https://www.kechuang.org/t/69517

闲的蛋疼,于是就有了这个无聊的想法
用汇编写程序就已经看起来很高大上了,那么直接用机器码呢?
其实机器码并没有想象中的那么困难,
机器码和汇编语言是一一对应的,因此知道了汇编语言,再学会转换的方法,就可以直接用机器码写程序了
本文将会分为两个部分,介绍如何用机器码写hello world
一、基础知识
写一个怎样的程序
计算机引导
mbr
x86
实模式与保护模式
逻辑地址
显存与内存映射
二、写程序
使用diskgenius写机器码
运行程序

废话不多说,让我们先从写一个怎样的程序开始
【你这不是废话吗,标题都写了是hello world!】
不不不,没那么简单,写完了程序在哪儿运行也是一个值得思考的问题
【这有啥子好思考的?双击运行呗!】
NO,没有那么简单。要知道cpu只能从内存中执行我们的程序,想让计算机运行程序,就需要先将程序放入内存。当你在windows里双击一个可执行文件时,操作系统会帮助我们将程序放入内存,再由计算机执行
但是若想要操作系统听你的话,你就必须按照操作系统指定的格式来编写程序。如果你不按照系统的要求来,他只会弹出来一个框,告诉你“xxx不是win32应用程序”
然后你就只有瞄着屏幕哈气了(我们数学老师的口头禅)
我们的目标是在屏幕上显示出一行hello world,考虑这些会大大增加实现的难度。
为了更加方便编(zhuang)程(B),扔掉该死的操作系统就好了
不过抛弃了操作系统,上面的问题还是没有解决:如何将程序放入内存?
要解决这个问题,我们必须从计算机的引导过程说起.
你可能已经知道,引导的英文是是“boot”,如果你比较冲动的去查了一下,你会发现它本来的意思是靴子。。。没错,就是靴子
原来,这里的boot是bootstrap(鞋带)的缩写,它来自一句谚语:
“pull oneself up by one’s bootstraps”,意思是拽着鞋带把自己拉起来,这句话牛顿爷爷听了肯定很不高兴。
在计算机刚刚发明的时候,工程师是灰常纠结的,为什么呢?要知道,计算机是从RAM里头执行程序的,工程师纠结的问题和我们差不多,如何把程序放入RAM。
这可以由一个程序完成,但是这样就意味着:必须先运行一段程序计算机才能启动,计算机要运行程序,又必须启动……于是就死循环了。。。
工程师需要想尽办法,把程序装入RAM,计算机才能启动,而这个过程他们叫做“拉鞋带”,久而久之就叫“boot”了。
之后,人们发明了ROM,人们将开机程序存在ROM里,这样每次开机,计算机就自动从ROM里执行这段开机程序。
这块芯片里的程序叫做”基本输出输入系统”(Basic Input/Output System),简称为BIOS。BIOS程序会将引导程序放入内存,然后让cpu执行引导程序,自己就去睡觉了
我们只需要将我们的hello world当做引导程序,其他的交给BIOS就行了。
BIOS实际上是将硬盘(严格的说是储存介质,包括光盘)上的第一个扇区(共512字节)放入内存,这个扇区叫做MBR( Master Boot Record,主引导记录 )
MBR分为三个部分,分别是
466字节的引导程序
64字节的分区表
2字节的校验位
引导扇区是用来放引导程序的(有引导的需要的话),我们这个程序没有什么需要引导的,因此直接将hello world放入这里就行了。466字节看起来很少,实际上对于显示hello world是绰绰有余的(我们仅是用了几十字节)
分区表没什么好说的,用来硬盘的分区信息。因为只有64字节,因此保存的数据是很有限的,这也是为什么一块硬盘上最多只能有四个主分区的原因——第五个主分区放不下…
最后的校验位很重要,正确的值是55aa(不然说明这个不是MBR)。
BIOS程序是很负责的,它在把这个扇区的内容复制到内存中后,会检查这两个字节,当它为55AA的时候,BIOS才会放心的睡大觉(不然说明这不是引导信息,BIOS会继续按照你设置的引导顺序检查别的驱动器,比如光驱)
x86
1978年6月8日,Intel发布了新款16位微处理器“8086”,也同时开创了一个新时代:x86架构诞生了。
这是一个复杂指令集(于此相对有“精简指令集”)
尽管过了这么多年,但直到今天,为了保证电脑能继续运行以往开发的各类应用程序,Intel公司所生产的所有CPU仍然继续使用(兼容)X86指令集。
(由于x86的效率实在是太低,inter早就想扔掉x86。但是在2003年,竞争对手AMD首先推出了64位cpu,x86-64,也就是传说中的x64,兼容x86。inter没有办法,只好也与2004年推出了兼容x86的64位cpu)
上面都扯远了,现在再说一说实模式与保护模式
保护模式与实模式相对应。在80286以前,CPU只有实时模式,在286之后增加了保护模式。保护模式下有保护机制,可以防止程序在运行的时候胡乱修改内存(比如数组越界,如果在实模式下,系统就可能崩溃)
但为了保证兼容(又是讨厌的兼容),80286及以后的CPU会首先进入实模式,然后通过切换机制再进入到保护模式。
在实模式下面,所有寄存器都是16位的(哪怕是64位的cpu也一样),在切换到保护模式下后,才会变成32(64)位。
我们只不过想显示一行hello world,就用实模式足够了,简单才是王道嘛!
来自 Linux   分享
全部只看作者
2015-1-18 19:31:13
相对论万岁百炼成钢(作者)1楼
前面讲到的都是一些需要了解的基础知识
不过知道上面那些东西还远远不够
本文就将介绍一些编程中必须知道的知识。
我们知道了BIOS程序会将引导程序加载入内存,问题是加载到哪儿呢?
这个问题值得讨论,因为不知道程序在内存中的位置,我们就很难控制程序的执行。
答案是0x7c00
为什么会选择这样一个奇葩地址,而不是0x0000之类的方便易记的地址,当然是有原因的。这里就不介绍了,有兴趣希望了解的可以使用百度、谷歌搜索。
086的寻址方式
因为8086的cpu是16位的,理论上来说,16位的总线最多能访问2^16=64KB的内存(16根总线一共可以表示65536种不同的地址,因为内存是字节寻址,一个字节1Byte,所以就是64KB),这么点内存在当时也是捉襟见肘的。
为了能让cpu控制更多的内存,就必须增加控制总线的数量,于是乎8086就设计出了20位的寻址总线,也就是2^20=1MB,这样内存总算够用了。但是寄存器还是16位的呀,怎么办呢。8086就使用了一种非常奇葩的方法,一个寄存器不够,那我们就用两咯,一个叫做段地址,一个则是偏移地址
我们把一栋楼房当作一块内存,一共有2000个房间,房间按照0000-2000顺序编号(这就是物理地址)。
但是由于偷工减料,现在一个门牌号只能写三个数字。为了解决门牌号的问题,我们可以把这栋楼分成多个单元(段)一个单元又分成多个房间。
假设一个单元有100个房间,那么0106号房间就是“二单元006”,其中二单元写在门牌A上,006则写在B上,这就是逻辑地址。
知道了逻辑地址,我们就可以把它换成物理地址,你应该看得出来,换算关系就是把门牌A的值-1,然后乘100,再加上门牌B的值。(2-1)*100+006=0106
在CPU中,门牌A就是段寄存器,保存的地址叫做段地址。门牌B则是指针寄存器,保存的值叫偏移地址。
在访问内存时,cpu将段地址左移四位,再加上偏移地址,就合成了20位的地址
如段地址为0x72,偏移地址为0x25,段地址偏移后的值为0x720,加上偏移地址就成了0x745.
因为16位最大是65536,也就是64kb,因此我们可以得出结论,每一个段最多是64k。
和上面楼房的地址不同,段是可以重叠的,比如你可以将0x100-0x1ff作为一个段,将0x101-0x200作为另一个段。也就是说一个物理地址对应多个逻辑地址
如何显示内容
在c语言中,我们可以很简单的使用printf来输出一个字符串
但是在汇编下可没有这东西。
我们需要直接控制显示器按照我们的需要显示信息。
如果你凑近看的话,就会发现显示器上有很多个小点,我们只需要控制这些小点,让他们以字符的样子显示出来就行了
你可能会跳起来说,这怎么控制!
别急嘛,且听我慢慢说。
对显示器来说,文字也好,图形也好,都是一堆堆的像素点,区别仅仅是他们的组合不同而已。但是如果让我们来控制这些像素,就显得非常困难并且麻烦了。
于是人们就制造了一个专门的硬件,让他来负责控制这些像素点。这就是显卡
显卡有一块内存,叫显存,我们只需要把字符写入显存,显卡就会帮助我们将他显示在屏幕上。字符使用的是ASCII码
由于显存是位于显卡上的,如果访问显存也需要通过显卡这个外围设备打交道的话,就显得有些麻烦了,毕竟多了一道手续了嘛!
于是聪明的人们想出来一个叫做“映射”的方法。即将显存当作内存(毕竟都是ram),映射到内存地址中,这样就可以直接使用cpu访问显存了。这块“内存”和普通内存没什么两样,唯一的区别就是你写在里面的数据显卡经过一定的处理显示在屏幕上而已。
086下0X00000-0X9FFFF是留给普通内存的,后面的0XF0000-FFFFF则是存放BIOS的芯片。中间0XA0000-EFFFFF的320k字节,则由各种外围设备提供,比如显卡。
显存的范围是0xB8000-0XBFFF,除非你的显卡出了毛病。否则这个地址肯定是可以访问的!
由于历史原因,显卡在启动后,会自动的被初始化为80×25的文本模式,一共两千个字符。
0xB8000就对应屏幕的左上角第一个字符,然后是往右依次类推
和用c语言的printf输出不同的是,一个字符并不是占用一个字节,而是两个。
第一个字节是字符的ASCII,第二个字节指定了字符的显示属性,比如颜色,是否闪烁(某些人想尽各种办法希望给命令行窗口中的文字加上不同的颜色,殊不知操作显存控制颜色竟是如此方便)
颜色分为前景色和背景色,格式如下
233905

RGB控制颜色,它们对应的意义都在下表中给出(数据摘自《X86汇编语言从实模式到保护模式》李忠、王晓波、余洁著,P51)
233906

比如我们要黑底白字无闪烁(也就是用的最多的模式),他的属性值就是
K=0,RGB=000;I=0,RGB=111,合起来就是00000111b
换成16进制则是0x07
假设我们需要显示一个黑底白字的无闪烁字母H,0xb8000的位置就是H的ASCII码(0x48),后面接着的0XB8001就是显示属性0x07.
你可能会好奇,屏幕上一片漆黑,啥都没有的时候显存里面是什么东西?
还能是什么?空格呗!

相对论万岁百炼成钢(作者)2楼
到这里我们终于可以开始动手使用机器码写程序了
不过在此之前,我们还需要做一些准备。
因为我们选择将hello world写入mbr中,而这样会覆盖掉引导信息。如果你直接在你的硬盘上操作的话,你的系统就不能启动了(尽管我们可以使用修复软件修复引导记录,但是那样很麻烦)
你需要的仅仅是一个u盘,或者储存卡+读卡器(这里便使用的储存卡)
然后是一台支持usb启动的计算机
【我怎么知道支不支持?】
只要你不是老掉牙的几十年前的计算机,都是支持的
【我们u盘里有很多文件啊,弄了这个会不会造成数据丢失啊?】
不会!我们只会修改前面引导程序的部分,不会修改后面的分区记录,所以保存的数据是不会丢失的。如果你不放心可以先备份
当然,如果你的u盘是“引导盘”,比如xxxPE装机的,由于修改了引导程序,自然就会失效……所以这类u盘谨慎尝试(大不了再制作一次嘛,或者在写程序时进行扇区备份)
如果你不想用u盘折腾,那也行。虚拟机的虚拟磁盘一样是支持的
至少vhd,vmdk之类的格式都是支持的。
我们需要对扇区直接进行操作,这需要专门的软件,但是我找了一圈,有两种编辑器,一种是好用的,一种是不好用的,在好用的里面却只有一种,那就是死贵的!
winhex貌似可以免费试用,但是界面是英文的,diskgenius是中文的,可是目前最新版不交钱不能编辑扇区
好在貌似diskgenius以前的版本是可以免费编辑扇区的,而我以前下载过,今天又从硬盘中把它翻了出来,你可以“点我下载”
下载后直接运行就是了,你会得到这样的一个界面


插上u盘,然后选择你要写程序的硬盘(注意是硬盘不是分区)
在卷标的下面有一个“扇区编辑”,点一下,就会成这个样子


如果你选择使用虚拟机
请创建一个固定大小的单文件磁盘,不需要多大,10MB都够用了!,内存更不用说,1M足矣!操作系统选择other(其他),并且不需要添加镜像!
然后在diskgenius里面的菜单“硬盘”->“打开虚拟硬盘文件”,选择你的虚拟磁盘文件,后面的操作都是一样的!

注意右边的“Error loading operating system”是不是有些熟悉呢?没错,你选择了没有引导记录的扇区就会显示这个,其实这样的错误信息也是MBR程序显示出来的。
往下拉一点,你应该看得到“55AA”,这就是传说中的校验位,也就是512字节的地方。
现在回到最开始的地方,我们开始写程序!
注意:如果你不想手动输入而是复制机器码,请复制后右键->写入->写入hex即可
机器码可以通过http://ref.x86asm.net/coder.html#xF3查询
这里也提供一个pdf版的inter开发文档点我下载
我们先来整理一下思路
为了便于操作显存,我们将显存从0xb8000开始作为一个段,也就是0xb800:0x0000,将它放入段寄存器ES(其实DS也行,不过后面DS有其他的用途)
完成这一步的机器码是:B8 00 B8,8E C0


像这样。
B8是mov指令,它将0xb800放入ax寄存器(因为是小端模式,00是低位,放在高位上)
然后我们将ax的数据送入ES寄存器,8E是mov指令(mov有好多不同的指令……),C0则代表AX。
然后,我们将从把屏幕“清屏”,就是将屏幕用黑底的空格填充,空格的ascii是0x20,黑底则是0x07。放在一起就是0x0720(小端)
我们使用循环填充空格的方法,需要在cx寄存器中放入循环的次数2000次
它是:B9 D0 07(2000的15进制是0x07D0)
B9=B8+1,1说明目标是CX(ax是+0,所以还是B8)
然后我们将0放到bx中当作偏移地址(这里有点像c语言中的数组,第一个元素的地址就是段地址,下标则是偏移地址)
机器码为BB 00 00
BB=B8+3,3就是BX,后面四个0不说你都知道。
接下来就是循环体了:26 C7 07 20 07
26是说明以ES作为段寄存器,前面我们已经将他改成了0xB800。
C7是把立即数放入内存的mov指令,07代表用BX提供偏移地址,2007则是数据
然后是81 C3 02 00
81是add指令,c3代表BX,0200就是0x02,说明这里将bx自增了2。因为上面一次性传送了2字节,所以偏移地址也要往后移动两字节
循环体到此结束,下面是E2F5,
E2是LOOP指令,后面的0xF5则是偏移地址,他是1Byte的有符号数,负数说明往前退,F5就会退到前面26 C7 07的地方,每执行一次,CX自动减1,直到cx为0。也就是说,会执行2000次。
接下来我们将为把字符串写入显存做准备
因为指令和字符串这样的数据是放在一起的(这种指令和数据放在一起的叫做冯诺依曼架构,还有一种数据和指令分别放在不同内存上的叫做哈弗架构。一般的计算机都是冯诺依曼架构,而CPU内的Catch则是哈弗架构)
为了将字符串写入到显存,我们需要知道字符串的位置
我们把程序加载开始的部分当作段地址,并且将它放入DS
B8 C0 07,结合前面的你应该知道这将0x07c0放入了ax,然后是8E D8
他将AX的值放入DS中
接着是FC,这是cld指令,因为我们后面需要传送字符串,因此需要将它清0,指定字符串的传送方向。
后面是BE 2A 00BF 00 00
这都是传送指令,BE是传送到SI,BF则是传送到DI,SI里面放的是字符串的偏移量,DI里面放的是显存中的偏移量,因为我们是从第一个字符开始写,因此偏移量就是0.
最后,我们需要再次指定循环的次数
B9 0C 00,他将hello world的长度放入cx,
0x0C是12个,即“Hello World!”一共十二个字符
倒数第二步,我们将字符串一个个的循环传送到显存,机器码为F3 5A
F3 A5代码字符串传送,就是rep movs。Move (E)CX words from DS:[(E)SI] to ES:[(E)DI].。他会一直执行到CX为0,也就是把hello world全部写入显存。
最后一步。cpu在执行完了上面步骤后并不会停下来,如果你不进行干预的话他会一直执行下去。这不是我们希望看到的结果,因此我们需要让cpu有事情干——死循环!
死循环的机器码为FD FF。其中FD是jmp指令,FF表示往前退1字节,也就是FD的位置。这样cpu就会不停的在这一句上跳转,也就是死循环了
程序到这里就算完了
【喂喂喂,我们怎么没有看到hello world字符串啊!】
急什么,这不是留在后面来设计吗?
前面已经详细的说了显存中显示字符的方法,这里就直接给出字符串了
48 8F 65 8F 6C 8F 6C 8F 6F 8F 20 07 57 02 6F 02 72 02 6C 02 64 02 21 02
我个人喜欢让hello 闪烁,然后world用绿色显示出来,黑底白字闪烁的属性是8F(可以看到前面每隔一个字符就是一个8F),黑底绿色不闪烁则是02
48 65 6c等则是hello world的ASCII
写到这里,我们的程序就算彻底搞定了
到这里你就应该明白DI的值为什么是2A了,你可以数数,字符串的第一个字节的偏移地址正是0x2A(以程序存放的地址作为段)
下面给出本程序完整的机器码
B800B88EC0B9D007BB000026C707200781C30200E2F5B8C0078ED8FCBE2A00BF0000B90C00F3A5E9FDFF488F658F6C8F6C8F6F8F200757026F0272026C0264022102
你也可以下载这个包含机器码的txt
写好了后是这个样子


红色代表数据被修改了,中间那个BE只是恰好和原来的数据一样罢了
最后点击“扇区编辑”左下边的保存即可

运行程序

搞了这么久,终于可以运行我们的程序了。
对使用u盘的等引导的人
首先你需要关闭计算机,别拔了u盘,然后再开机,刚刚出画面时根据提示按相关的按键。通常是F2,del,esc等等,总之你需要选择引导的磁盘(不清楚的可以百度,或者回复问我)
选择U盘启动,然后略等一下!就是见证奇迹的时刻!


对于使用虚拟机的人
你只需要启动电源,然后就可以看结果了!

bitravel 发表于 2019-9-19 15:55:51

本帖最后由 bitravel 于 2019-9-19 17:06 编辑

我来操作演示!
步骤1:生成一个大小为1.44MB的空软盘!随便一个二进制编辑器或者磁盘工具都可以。
操作视频链接:https://pan.baidu.com/s/1XvE5rETAmSRbiIdp0XfJRw

yuweb 发表于 2019-9-19 16:53:49

围观。。。

bitravel 发表于 2019-9-19 17:08:01

本帖最后由 bitravel 于 2019-9-20 09:23 编辑

2.复制写好的二进制机器码到这个空白虚拟软盘的头部!作者已经给出二进制机器码
B800B88EC0B9D007BB000026C707200781C30200E2F5B8C0078ED8FCBE2A00BF0000B90C00F3A5E9FDFF488F658F6C8F6C8F6F8F200757026F0272026C0264022102
同时别忘了在第一扇区的尾部写入0x55和0xAA(小端序)!
操作视频链接:https://pan.baidu.com/s/1FN-sa8Sf_p4NddLrfFoeNg

bitravel 发表于 2019-9-19 17:08:41

本帖最后由 bitravel 于 2019-9-21 07:48 编辑

3.在虚拟机中测试!虚拟机的基本操作这里就忽略了!
操作视频链接:https://pan.baidu.com/s/15_waeOP6pJ-yDemKYAeUzw
boot.img下载地址:链接: https://pan.baidu.com/s/19qOfeeZQC7Lp69hjJ24NxQ 提取码: 5wxc。

bitravel 发表于 2019-9-19 17:22:22

本帖最后由 bitravel 于 2019-9-20 09:32 编辑

我们用反汇编工具来看一下这串二进制代码的意思,参考文章:https://blog.csdn.net/m0_37329910/article/details/93379045,https://stackoverflow.com/questions/10362630/how-can-objdump-emit-intel-syntax

使用objdump工具:objdump -m i8086 -M intel -b binary -D boot.img
显示结果:
boot.img:   file format binary

Disassembly of section .data:

00000000 <.data>:
       //注:机器码开始
       0:        b8 00 b8                     mov    ax,0xb800
       3:        8e c0                        mov    es,ax
       5:        b9 d0 07                     mov    cx,0x7d0
       8:        bb 00 00                     mov    bx,0x0
       b:        26 c7 07 20 07               mov    WORD PTR es:,0x720
      10:        81 c3 02 00                  add    bx,0x2
      14:        e2 f5                        loop   0xb
      16:        b8 c0 07                     mov    ax,0x7c0
      19:        8e d8                        mov    ds,ax
      1b:        fc                           cld   
      1c:        be 2a 00                     mov    si,0x2a
      1f:        bf 00 00                     mov    di,0x0
      22:        b9 0c 00                     mov    cx,0xc
      25:        f3 a5                        rep movs WORD PTR es:,WORD PTR ds:
      27:        e9 fd ff                     jmp    0x27
      //注:机器码结束
      //写入文本模式的显存中的数据开始
      2a:        48                           dec    ax
      2b:        8f                           (bad)
      2c:        65 8f                        gs (bad)
      2e:        6c                           ins    BYTE PTR es:,dx
      2f:        8f                           (bad)
      30:        6c                           ins    BYTE PTR es:,dx
      31:        8f                           (bad)
      32:        6f                           outs   dx,WORD PTR ds:
      33:        8f                           (bad)
      34:        20 07                        and    BYTE PTR ,al
      36:        57                           push   di
      37:        02 6f 02                     add    ch,BYTE PTR
      3a:        72 02                        jb   0x3e
      3c:        6c                           ins    BYTE PTR es:,dx
      3d:        02 64 02                     add    ah,BYTE PTR
      40:        21 02                        and    WORD PTR ,ax
      //写入文本模式的显存中的数据结束
      //...省略号中的内容全部为0
        ...
      //0xaa55标识
   1fe:        55                           push   bp
   1ff:        aa                           stos   BYTE PTR es:,al

发表于 2019-9-26 11:43:43

本帖最后由 SВ 于 2019-9-26 11:48 编辑

对指令的详细注释:
/*** 注:机器码开始 *** /

//es = 0xb800,表示段首地址是显存内存映射区(文本模式下)
0:      b8 00 b8                     mov    ax,0xb800
3:      8e c0                        mov    es,ax

//cx表示下面填充空格符的次数
5:      b9 d0 07                     mov    cx,0x7d0

//bx表示指向段首地址是显存内存映射区的元素(每2个字节算一个元素),初始化为0,表示指向第一个元素
8:      bb 00 00                     mov    bx,0x0

//不断循环,将屏幕用空格符清空
b:      26 c7 07 20 07               mov    WORD PTR es:,0x720
10:      81 c3 02 00                  add    bx,0x2
14:      e2 f5                        loop   0xb

//ds = 0x7c00表示当前程序的起始物理地址的段首地址,也是用于寻址下面的“hello,world!”字符串
16:      b8 c0 07                     mov    ax,0x7c0
19:      8e d8                        mov    ds,ax

//初始化复制“hello,world!”字符串的方向,用于rep movs指令
1b:      fc                           cld

//初始化:si指向源字符串的首个字符,di指向目的字符串的首个字符
1c:      be 2a 00                     mov    si,0x2a
1f:      bf 00 00                     mov    di,0x0

//cx表示一共要重复复制的次数
22:      b9 0c 00                     mov    cx,0xc

//上面的准备工作完成,开始使用rep movs指令复制字符串
25:      f3 a5                        rep movs WORD PTR es:,WORD PTR ds:

//死循环
27:      e9 fd ff                     jmp    0x27

/*** 注:机器码结束 ***/

cnpzhlq 发表于 2019-9-26 16:50:05

好高级的想法,从来没用过机器码。

发表于 2019-9-27 13:51:59

bitravel 发表于 2019-9-19 17:08
3.在虚拟机中测试!虚拟机的基本操作这里就忽略了!
操作视频链接:https://pan.baidu.com/s/15_waeOP6pJ- ...

用到的软件是磁盘软件diskgenius和十六进制编辑器Hex Editor Neo。

dywm 发表于 2019-10-15 17:44:35

{:10_256:}学习了

Yvonne! 发表于 2019-10-18 22:40:55

楼主,很能折腾o(∩_∩)o...哈哈!!!

顶一个{:5_109:}
页: [1]
查看完整版本: 用机器码写开机能运行的hello world!