How can I load code to a microcontroller?
Yes, the ATmega328 flash memory, and fuses, are programmable on a brand new, unprogrammed chip (no bootloader) over its SPI interface. So you need to control four of the ATmega328 pins; the SPI interface and RESET.

Read section "31.8. Serial Downloading" in the full ATmega328 datasheet (not the summary datasheet), which begins:


  Both the Flash and EEPROM memory arrays can be programmed using the
  serial SPI bus while RESET is pulled to GND. The serial interface
  consists of pins SCK, MOSI (input) and MISO (output).


One way to make your own In-System-Programmer (ISP) is to use an Arduino. There are examples of how to control the SPI interface of the target ATmega328 on the web, but you could start by reading the code in Using an Arduino as an AVR ISP (In-System Programmer). It is a 'full' ISP programmer, which works with avrdude, which is an Open Source programming application. You will need to figure out how to build a binary whoch avrdude can read. Potentially the easiest way to start is to use the GNU gcc binutils assembler and linker. They will produce a binary whch avrdude can upload with no other 'moving parts' (applications).

If you want something even more minimal, you'll have quite a lot of work to do. However, a useful step might be understand how the SPI upload works. You might do that by extracting the upload code of that Arduino AVR-ISP program, throw the rest of the program away. Let's call this the 'upload program'. Then, instead of implementing a protocol to upload the binary program from avrdude, store the binary code that you have created representing your program, in that new upload program as data. Modify the program to upload that data using the upload code. This gives you a self contained upload system, which does need the avr-gcc tool chain without using the Atmel IDE. When you load the combined upload and binary-program data onto an Arduino, it will upload to a ATmega328. 

You can then figure out how you want to proceed from their, knowing you have a working upload system. For example you could then invent your won protocol to talk to your own uploader, and your own format for the binary code.

Edit:
A bootloader is loaded onto an unprogrammed ATmega this way. However, you don't need a bootloader if you have something which will program the ATmegas flash and fuses over SPI.

There are several Open Source ATmega bootloaders, so you could use own of those, or use one as the basis of your won.

I would suggest you look at avrdude, which might be a adequate fit for uploading code. I have not read its source, but a close friend has, and said it is very complex because it works with almost all Atmel chips ever made. He wrote a simple SPI loader, which only worked for a single ATmega, from scratch in a few days.

I strongly recommend looking at GNU binutils for AVR. You get all of these when you download and install the Arduino IDE. They include an assembler, which will be much more productive than hand assembling AVR instructions, and a linker which will produce 'proper' binary files that programs like avrdude can upload. The binutils also have handy tools like a disassembler (for properly formatted binary code files).

Edit 2:
@AndrewP has pointed out that a system like a Raspberry-Pi can be connected electronically to an Atmega (either with a bit of level shifting, or by running the ATega at 3.3V) so that it can drive the SPI interface directly. Or, if you have an old enough PC you might also use the parallel port.

(Personally, I'd suggest looking at FTDI USB to SPI cables, or their general purpose USB interface hardware which would allow you to program the SPI interface from a host PC over USB)

(Also, IIRC avrdude runs on the R-Pi. I haven't checked, but there is likely a version of the GNU binutils for the R-Pi already compiled and ready to install.)