Nicely structured code! I'm almost tempted to port it to amd64, just for fun. After all we really don't need any more x86/16bit example code, we need more amd64 code! :-)
I've been saying this to my co-workers over the past week as I've gotten into modern day assembly (ARM, but still): It's basically a high level language at this point! It's amazing how much structure you can actually give yourself with modern tools.
I'm not sure you really need much in the form of modern tools to achieve this -- the code base above is basically include macros and jump labels?
As mentioned by agumonkey, it feels a little bit like Forth.
In fact, I seem to recall a rather excellent tutorial on 68000 assembly featured very similar structure (even if I never finished it -- at the time I was defeated by my own bugs and the fact that those bugs, without an MMU or memory protection, could easily crash the entire machine. It was rather demotivating...).
For another example, see the original source of Apple II DOS, posted here a while back:
Thanks for wasting 4 minutes of my life! For some reason, I picked up a Wyse Winterminal S50 from Weird Stuff last week... a thin linux client box from 2006 that has some sort of underpowered x86 and very little memory.
So I was playing with it and it has this weird linux on it that lets you set it up to very slowly browse the web and such, but it wasn't very interesting, all told.
When I saw this project come up, I grabbed the .img, wrote it to a spare usb stick and damn if the thing didn't just boot up to floppy bird. Nice work! Now I wonder what other dumb PCs I have sitting around that can boot it...
Turns out after you have written the floppy IMG to a blank
uFD or HDD, and it boots and plays the game, then you can
partition the drive (I used Linux Mint), format it as
FAT32 or NTFS, and use the full drive for mass storage
without a conflict.
Same drive still boots to Floppy Bird only, unless you
install some other OS or something which overwrites
the Floppy Bird bootloader on sector 0, and give up the bird.
Partitioning and formatting alone do not overwrite the MBR,
even if it is just a custom floppy boot sector at the time.
It's also not really running on "RAW METAL" because it's using BIOS interrupt routines for drawing and input.
It's still awesome, though, and the code is really cleanly written and easy to follow.
Because the code is so nice, this would be a fun project to try to port to Protected Mode with its own HAL and maybe drivers for one set of hardware (perhaps QEMU's). Could be a fun experiment in why OSes are hard and x86 is painful.
vga.asm copies bytes directly to video memory in a raw manner, once it's in VGA mode. It only uses BIOS to switch into VGA mode ("mov ax, 0x13" and then "int 10h"), and back to text mode before rebooting when you quit. It does use BIOS to read the keyboard. It also reads bytes from sectors of the disk using BIOS rather than the OS when it starts up. The sound is a beep created with raw "OUT" commands on the data ports (for the PC speaker presumably).
In other words, it's using BIOS for input, but not drawing.
ticks() and sleep() also use BIOS. The author could have implemented this by handling IRQ 0. :-) Come to think of it handling the keyboard interrupts isn't that hard either...
Also looks like boot.asm uses the BIOS to read the rest of the program...
Seems like if the "boot.asm" (actually sector 0 of the floppy) could be made relocatable away from sector0,
then it could still be functional when stored in a filesystem (including on a HDD) not unlike BOOTSECT.DOS.
The remainder of the floppy (sectors 1 through 16 proper) when copied to the same sectors (which are normally blank
and unused) on a MBR/BIOS style HDD work just the same as from floppy without having to be in a filesystem
themself.
Then you could easily chainload to Floppy Bird on a HDD from other bootloaders (like WinNT5 & NT6), with no changes to the main ASM program.
EDIT: the comments in boot.asm refer to sectors 1 & 2 since they are the first 2 sectors of the floppy, these two are officially numbered 0 & 1 in a disk editor or DD command.
This stuff is kind of annoying to write. When I wrote stuff on raw metal (hobby OS kernel) I mainly just built an ELF file that GRUB can load. (Surprisingly easy to do.) Or, in days before that when I used more Microsoft stuff, a small .COM file where ms-dos or freedos is a glorified bootloader. (DOS is totally cool with a program hijacking everything.)
And in principle you can write in a higher level language and build for a target without an OS. In practice, you can certainly write the bulk of your code in C and only write (possibly) a little initialization and (possibly) some IO code in assembly - mostly inline assembly.
First of all, this is really cool and I think the closest thing I have seen to 'readable' assembly.
Tiny nitpick: doesn't running asm still require an operating system? I haven't read all of the code but I assume that there are instructions for I/O or other traps that require the OS. Isn't the OS just the layer between the hardware resources and programs that want to access those resources?
No. ASM is just a slightly easier mnemonic version of the raw machine language. All software basically compiles down to this form.
The OS functions are provided here. The "game" is just an application in the OS (kinda). The BIOS provides whatever the OS needs to hook into to use the hardware. Here's a high-level overview of how this works http://duartes.org/gustavo/blog/post/how-computers-boot-up/
OS's don't need to be very complicated, just a few bytes in some cases depending on what you want to do. For example, you can just use the standard VGA modes (here, he's using 13h) to get something to display, and just write directly to the video memory. The BIOS helps handle some of the interface work to the various standard components.
This is the real old-school way of doing things and the code is really well done and commented. If you know x86 asm basics, you can read through most of the system-level stuff without too much trouble.
I think the Floppy Bird uses Real Mode[1], which provides an easy access to various devices directly through the BIOS. Practical opearting systems switch to protected mode, which provides more features, but doesn't allow you to use BIOS interrupts (and at that point you need to start writing device drivers).
Here, that layer doesn't exist. This program takes the place of the OS, and communicates with the hardware directly, without an OS in the way.
This can be done because ages ago, the PC hardware interface was almost completely standardised, and all PCs to this day implement that standard for backwards compatibility.
It's a Real Mode (16-bit) 'OS', very similar to DOS. When you run in Real Mode, the BIOS's provides a few hooks for doing some basic 'syscall' type stuff like changing the video mode, so you can get away with doing less in the actual OS code by doing it via the BIOS. That said, this code he has will run on any hardware that'll run DOS (for the most part).
If he did this via 32-bit assembly he'd have a fair amount more code to get things done (Though he still uses very little hardware anyway so he could get away with not setting up most stuff).
The BIOS has a lot of built in traps that, while minimalistic, make all PCs appear the same to you, the trap-user. This is the bare minimum to load an OS + drivers from disk, or run an OS installer, or turn your PC into a flappy bird arcade machine.
Come to think of it, "bare metal" would really be modifying your BIOS to contain flappy bird. Then you could draw to the screen directly. Too bad it would only work on one model of motherboard. :)
If you write ASM that makes system calls, yes. If you write ASM that just talks directly to your devices, no.
For the latter, ASM isn't even strictly necessary outside possibly some narrow things that could (/should, if this was a "real" project) be inline assembly in a C program. Obviously, your C couldn't call any system calls and (depending on the runtime) might even need to forego malloc and similar.
I haven't looked at the code, but I assume this just uses the BIOS in your PC. The BIOS is a basic layer for talking to raw hardware, whereas Operating Systems normally are expected to provide many higher level functions (file systems, processes, virtual memory, etc).
True, you could leave out the bootloader and some of the sys code and this would then nearly be a DOS program, at least the port to DOS would be easy. Leaving it as is allows it to boot without an OS.
That is a rather narrow (formal?) definition of OS. If instead you consider an OS to be (more generally) a collection of system-level software providing the interface between hardware and the userspace application, then in this case the sys code is the OS and the game code is the userspace. This definition is based on the idea that an OS manages resources and provides services - as opposed to the more rigid definition that an OS manages resources including processing time and storage space.
I'm wondering: how hard would it be to add sound to this thing? I think it uses Piezo sound, which is probably easily addressed. But what about midi or PCM output? Would you have to target every manufacturer specifically?
I really liked how fast my computer "booted" into the game. I would totally start collecting games like these if there were a scene.
Assembly is about as nonportable as you can get. Hardware access (not exposed by other languages), speed, and possibly executable size are the practical reasons one would write in assembly. The second and third are almost entirely outdated - storage of executables is free relative to storage of data; hardware speeds have made "slow" programs plenty fast for most things; and compilers can do better than you can quickly do, very quickly. The first is still a valid reason to use assembly, but typically in the form of a small snippet of inline assembly in C/C++ code.
> Hardware access (not exposed by other languages), speed, and possibly executable size are the practical reasons one would write in assembly.
This is a bit misleading, accesses to physical memory, disks and to other devices are usually protected by the operating system, not by the language. So you don't have direct hardware access because you program in assembly, but because you run a program without the control of an operating system. This can be done in assembly, but also in C or in any other language that can generate native code.
The only thing that you can do with assembly and that you can't really do with higher level language like C is accessing processor specific registers/instruction sets (e.g. SSE). And this is typically done using inline assembly snippets, as there is no reason to write a whole program in assembly when you only need to optimize a single function.
So I would say that having better hardware access is not a good reason to write an entire program in assembly.
On the other hand, building small, standalone executable file that use very little (stack) memory is still relevant for programs that run on very small (usually embedded) hardware. Especially when they need to run fast or to have low energy consumption. So speed and memory usage are still pretty good reasons to use assembly :)
Hmm, I guess there is some room for misinterpretation.
First, you seem to think I was answering a slightly different question - "In what circumstances should I make the decision to write assembly?" The question I was answering was closer to "When people need to write assembly, why?"
The actual question, of course, was not quite either of those.
Also, addressing a point of ambiguity:
I did not mean "hardware access ([this is something that is] not exposed by other languages)" - that is, in general, wrong.
I meant "hardware access ([when the particular thing is] not exposed by other languages)". This includes SSE (though that is starting to be included as builtins in some compilers). Outside of SSE, I grant that there isn't much on x86 platforms that is relevant to a game, since games on x86 platforms typically run under an OS. It can be relevant if you're writing a driver, though. And this can vary substantially by platform.
And no, it's not a good reason to write an entire program in assembly unless a sufficient percentage of your program is calls to these things that the C inlines are just adding cruft.
"On the other hand, building small, standalone executable file that use very little (stack) memory is still relevant for programs that run on very small (usually embedded) hardware. Especially when they need to run fast or to have low energy consumption. So speed and memory usage are still pretty good reasons to use assembly :)"
No, that is substantially more misleading. Like I said, you cannot quickly do better than a modern compiler, and a compiler does it very, very quick.
> I meant "hardware access ([when the particular thing is] not exposed by other languages ...
Ok, I agree.
>Like I said, you cannot quickly do better than a modern compiler, and a compiler does it very, very quick.
I'm not sure what you mean by "quickly" but this submission proves that it can be done in reasonable time. And if you look at the code (for example here:
https://github.com/icebreaker/floppybird/blob/master/src/gam...), it's obvious that you cannot produce anything nearly as compact and efficient with a compiler.
So, claiming that assembly is still a language of choice nowadays was maybe exaggerated, nevertheless it's nice to remember that no matter how good they are, compilers come with a cost. If you need some evidence simply compare the submitted code with any code produced with a compiler.
"it's obvious that you cannot produce anything nearly as compact and efficient with a compiler."
It's not obvious. We can argue over what's "nearly", but without even trying, manually converting the assembly to C I get 71 (1.5x) instructions out of gcc and that includes an inlining of animatebird. The resulting code is otherwise not so far from the original. It certainly reads more poorly, but that's to human eyes not x86. I'm not sure which would be more performant, and playing more carefully with the code and optimizations it can probably get even better.
Well, on Android the original Flappy Bird was 894kb in size,
and this Floppy Bird is only 8.7kb.
Plus the 8.7kb includes enough utility code to obviate the need to load an OS for the game to run on top of, so the size ratio actually is better than the apparent 100:1.
"Well, on Android the original Flappy Bird was 894kb in size, and this Floppy Bird is only 8.7kb."
You're actually making my point. Virtually the entirety of both those numbers is going to be image (and, on the Android version, audio data). Using lower-resolution assets is not something reserved to assembly language, and the size of the code is just not an issue next to that, most of the time. The biggest exceptions on desktops/servers being when executable makes the difference between spilling instruction cache and not. Things can get tighter on embedded, but there too tables and stuff are more often the cause of contention.
"Plus the 8.7kb includes enough utility code to obviate the need to load an OS for the game to run on top of, so the size ratio actually is better than the apparent 100:1."
Enough code to obviate the need for an OS (for this narrow use case, in Real Mode with BIOS calls available) is probably fewer bytes than enough metadata to convince Android to run you. But as mentioned, you're not actually comparing code size at all really.
"The TAB key changes the bird's color scheme."
I'm not sure what your point is here. Cycling palettes is a neat trick, but nothing new, nothing complicated, and certainly nothing that couldn't have been done as easily in another language.
Edited to add: Perusing your other comments, you seem to have plenty more savvy than your comment would show... were you trolling or trying to make some oblique point that I missed or what?
Well, this is a time I didn't really have an opinion
except maybe I like for things to take up less space for
the same functionality. I was just putting up the
numbers that I observed.
I didn't realize the Android version might have been
more sizable due to graphics & audio, so I guess
the functional code difference might not be as dramatic
as the 100:1 download size ratio after all.
The Tab key function was just another observation that
I had not seen in the documentation, don't even know
if it is intentional.
Check my other comment on this topic, it would be good
to get some feedback from the author.
>The only thing that you can do with assembly and that you can't really do with higher level language like C is accessing processor specific registers/instruction sets (e.g. SSE). And this is typically done using inline assembly snippets, as there is no reason to write a whole program in assembly when you only need to optimize a single function.
The other three most important things you can do in assembly that you can't do in C are:
* Indirect jumps (aka the "labels as values" or "computed goto" extensions admitted available in GNU C)
* Directly accessing status flags like the carry flag (necessary for efficiently implementing bignum arithmetic and CPU emulators)
* and optimizing register allocation across complicated control flow (like language interpreters or emulators)
It can absolutely be worthwhile to write an appreciable portion of a program in assembly if these "features" make the code significantly faster or even easier to express.
'It can absolutely be worthwhile to write an appreciable portion of a program in assembly if these "features" make the code significantly faster or even easier to express.'
Strictly speaking, yes. I would urge some hesitancy in concluding that is the case, though.
WRT the first, you take some portability penalty for using GNU extensions, but that is probably significantly less than you take by writing in assembly. Unless you're targeting an architecture GCC does not target and your alternative compilers don't have a similar extension, you should still not be writing assembly for this reason alone.
"Directly accessing status flags like the carry flag (necessary for efficiently implementing bignum arithmetic and CPU emulators)"
This falls under both "hardware access" and speed. It certainly is a reason to occasionally break out the inline assembly.
"and optimizing register allocation across complicated control flow (like language interpreters or emulators)"
This is a very bad reason until it actually proves strictly necessary (at which point it falls back under speed and/or size). Modern compilers are quite good at register allocation and getting better, and chip architectures are complicated enough that you're likely to be wrong about what's fastest and/or best. The ability to rapidly try new things will probably get you faster results faster - compare the time taken tweaking optimization flags and rebuilding, versus working out an alternative register allocation and threading it through all the relevant code. Moreover, a small change in requirements can entirely invalidate all of that work. An understanding of assembly and your target architecture sufficient to allow you to allocate registers by hand is still highly valuable when you get into that space, though, so you understand what you're looking for when comparing generated code and benchmark results.
I don't really agree with that, unless you want to call any use of a CPU "hardware access," but ok. I also don't consider it to be like accessing architecture specific instructions like SSE, because I simply cannot think of a general purpose CPU from the dawn of the micro that lacks the basic carry, overflow, zero, and sign flags, and the ability to branch on them.
>WRT the first, you take some portability penalty for using GNU extensions, but that is probably significantly less than you take by writing in assembly.
In practice, for a lot of developers, there are only one or two relevant CPU architectures, and they're almost always from the set of 32 and 64-bit x86 and ARM. Depending on what kind of program you're writing, it could be much more likely that you'd switch C or C++ compilers than switch target architectures. And if you are programming for obscure microcontrollers or whatever, you've pretty much thrown portability out of the roof to begin with and writing the whole program in assembly is not out of the question.
>Modern compilers are quite good at register allocation and getting better, and chip architectures are complicated enough that you're likely to be wrong about what's fastest and/or best.
Compilers are pretty bad at optimizing virtual machine interpreters, and this is unlikely to change. Here we go, straight from the horse's (or, uh, Mike Pall's) mouth:
"Compilers are pretty bad at optimizing virtual machine interpreters, and this is unlikely to change. Here we go, straight from the horse's (or, uh, Mike Pall's) mouth"
This could possibly fall into my "strictly necessary" bucket. I'm not sure what the timing differences actually amount to, either - I've got a threaded interpreter in a fast path in some of my code, and it hasn't proved a bottleneck (less than 1% of time during a low-latency event is spent running the programs) so I've not looked closely at register allocation through it.
I reiterate that this comes at severe cost to ease of extension and refactoring, however, and shouldn't be undertaken lightly.
Does anyone still use assembly for embedded? Of course. Is it a good idea? For some of them, and for the reasons I listed (I suppose I should add "you are working on a legacy assembly codebase").
"Still used pretty much everywhere" is just not the case anymore, even in embedded. At least, that's what the chart seems to say and it matches my experience working alongside embedded developers, knowing some embedded developers I've never had the pleasure of working alongside, and doing a small amount of embedded work myself.
(Edited to remove: Right in that link, both C and C++ are used more than assembly in the embedded space, and Arduino (more C/C++) follows closely behind assembly. The chart is unclear, but the rankings seem to be based on numbers that don't change when different filters are applied, which would mean that it does not show this. It is still certainly consistent with "assembly no longer dominates embedded", though - some of the C and C++ will be embedded, and all of the Arduino will be embedded.)
The person who wrote this wrote it to be booted directly from BIOS. That means it must handle everything an OS would otherwise do for it: bootloader, resource management, hardware access, random number generation, etc. So a project like this has great value as an introduction to low level systems and OS development.
As someone who has written no-os code, you do not need to do everything the OS does. If you do not mind giving up most of your RAM, you can stick to realmode and take advantage of the BIOS functions (you can work around the RAM limitation by jumping between modes). If your computer has UEFI, you have access to a much more advanced set of functions, and (for many purposes) can target that. I haven't done anything beyond hello world type stuff on UEFI, but it feels like you can get alot done without noticing the lack of an OS.
UEFI is awesome in terms of what it enables you to do without an OS, but once you install a few tools like an EFI shell, it's at least as much of an OS as DOS ever was.
I would love to see a chart of when each of these ports came out. It would be a pretty interesting representation of the difficulty of building the exact same thing in various languages.
https://github.com/icebreaker/floppybird/blob/master/src/mai...
Nicely structured code! I'm almost tempted to port it to amd64, just for fun. After all we really don't need any more x86/16bit example code, we need more amd64 code! :-)