How, exactly, does binary code actually become what is shown on a computer screen?

It is often something that is portrayed in media: “hackers” looking at ones and zeros all day to code. Of course, anyone who has extensively used computers knows that this just is not the case. But, few people actually think about how “ones and zeros” become what is seen on a computer screen. How exactly does binary code become codified programs/operating systems, and how do those programs/operating systems become pixels on a screen?

It depends on the context.

The simplest context is one of a terminal, where only text is displayed.
(google “bash terminal” to see one common linux example).
Terminals are obviously very important when working with computers on a low-ish level.

When a program is interacting with a terminal there is some implicit code to convert between binary and text. A common code is called ASCII (google it to see examples. ASCII encodes 7 or 8 bits (depending on how you count) to letters, digits, and some common punctuation. (ie. the number ‘65’ in binary is ‘A’ according to ASCII)

Thus, in a computer language you have a function that takes a set of bits (typically arranges as a series of bytes (8 bits), where each byte represents one charector). This function, ( in python it is “print”), then tells the operating system to put that series of bytes on the terminal. The terminal then converts the bytes to text according to the code (such as ASCII).

These days ASCII is not really used, as it only pretty much represents English. Instead a much more complex system called UTF is used. But again, it is essentially just a code book that tells how we go from a series of zeros and ones to some charector.

This at least gives you some key words to google.

The microprocessor reads numbers in from memory; these are then de-coded into instructions (e.g., a\leftarrow b+c) and data that are fed into building block execution units like arithmetic logic units that do the actual addition, multiplication, etc.

It depends what kind of “hacking” you are talking about, but on the software level there is usually no need to look at actual ones and zeros (machine language). Instead, programs are represented in a high-level language which could be anything from human-readable assembly language to a high level language that has to be compiled.

re. peripheral displays, the monitor itself has its own hardware which accepts digital inputs (e.g. red/green/blue values computed by the graphics card) and drives individual pixels

To address the OP satisfactory is pretty close to a one semester computer systems class.

Perhaps it can be broken down into smaller chunks. But a lot depends upon how you want to look at things as well.

In the most abstract, a computer is a device that modifies information, and the manner in which that modification occurs (the computer’s instructions) is controlled itself by information. Which very quickly leads to the realisation that is is possible for a computer to create new instructions, since they are just more information.

You can craft hardware that uses information stored in the computer to control how a display device behaves. Perhaps the simplest readily understood device is the humble ASCII printer. Information is usefully managed in chunks, where the size of the chunk uses a certain number of bits. The default minimum size chunk used for most machines is 8 bits. The minimum size is usually also called a byte. A byte can represent 2^8 = 256 different things. One set of things it can usefully represent are characters. Historically 7 of the 8 bits were used to represent characters from the Roman alphabet plus other useful characters. 2^7 = 128 possible characters. However a number of these were reserved for other uses. Since the general goal was to print readable stuff out there needed to be values that did other useful things, like carriage return and line feed, tabs, and various codes that let the computer and the printer coordinate the flow of information. Anyway, it isn’t a huge stretch to imagine a way that something akin to an electric typewriter could be made to type individual letters in response to individual bytes of information, if they were sent those bytes in some appropriate manner. Like in a sequence of electrical pulses over a wire. These words you are reading right now have been encoded with almost exactly the same mechanism. The only additional work is to add more characters and handle multiple fonts. But under the skin, there are bytes of information, and they code each individual letter. The browser you read this is is, to some extent operating as a very fancy electric typewriter.

But htose bits in a computer can be used to represent anything you like. Often there are conventions when you need to exchange information with others. But inside the computer, they are just arrays of bits. An array of bits 32 wide can represent 2^{32} = 4 billion things. A really useful thing is to use such lumps of data to represent numbers. So useful in fact that the computer has special instructions that allow number so represented to be manipulated. Same for 64 bit things. You can choose a few different ways to represent numbers inside these wide chunks of data. Integers, unsigned integers, floating point numbers are the main ones. These can come in different sizes to make best use of the system. But your chunks of information can represent much more.

The most important next use of information is as instructions that control what the computer does. Each instruction changes the state of the computer system in a specific manner. It might add two numbers. It might write a byte to a special place that causes it to be sent to a printer. And the best one, it can test the state of some unit of information and change the next thing it does based on the result. Programs are long lists of instructions that change the state of information in the computer and may change the order of how they perform changes based upon the state of the information.

Jumping a long way forward, but realised very early on in the history of computing, it was realised that it would be possible to write a program that traversed a slab of data containing instructions written in ordinary human words, encoded as those printable bytes we saw earlier, and would be able to translate that into instructions for the computer. The earliest of these were simple ways of writing down the names of computer instructions in a human readable form, and translating those into the instructions that would actually control the computer. It was quickly realised that much more powerful ways of expressing what the computer should do could be expressed in some sort of simple language. Enter languages like Fortran, COBOL, and Algol. The shadow thrown by Algol in particular is very long, and most modern programming languages have a direct lineage back to Algol.

Most computers are now programmed is some sort of high level language. Even when there is a need to directly manipulate information in its native form (actually looking at the bits themselves) some higher level languages provide ways of doing so. Other languages are designed in such a manner that it is impossible to manipulate things at the bit level.

In a modern personal computer system information is generally displayed on a graphical display device. All modern displays are just a rectangular array of coloured squares, picture elements or pixels. The colour of each square is expressed inside as chunks of information. Generally it is enough to have 256 levels of each or red blue and green to express the colour of a pixel. So three bytes of information conveys the colour. A device can be built that simply contains a slab of computer information storage, and it so constructed that it interprets information in that slab as colour values for each pixel in a display. Programs are crafted that write values into the slab of information to create patterns on the screen. Patterns can be a simple as lines or little rectangular arrays of values that, when displayed, are letters of the alphabet. So, it isn’t hard to imagine a program that is designed to take 8 bit characters (the same as we saw at the start) and use the values in those bytes to select different letters to be placed on the screen.

There is a huge amount more. Like I said, a one semester course easily.

To address the trope of a hacker watching the bits. Yeah, mostly this is a fiction. But it is based on some truth. Presented with a slab of information inside a computer, a hacker may need to work out what the heck he is looking at and work out what is going on. Like I said, you can represent anything you like with the bits. But there are conventions, and if you know what sort of computer, he may recognise computer instructions, and start to be able to work out what a program does, without needing to see the original human readable program. Back in the day this was a more common skill than it is now. I have colleagues that can still decode octal dumps of CDC 6400 code by eye. Another arcane skill is to watch network packets go by and recognise what is going on. One might guess that there are programs that can do all of this for you. But understanding how a system operates from the top to the bottom is a requirement for any hacker (black or white hatted.) This is because this gives you visibility of mistakes and subsequent holes in the system.

I especially liked working with eForth, which is a version of the Forth interpretive programming language designed for the IBM PC. My version is dated 1990. You can create the whole system from the manual, if you have MASM (the Microsoft Assembler, which lets you write programs in Assembly Language). Starting from the ground up, you type in assembly code to create 29 “words” or functions in Forth. At that point, there’s enough Forth that you can do all the further building in Forth itself.

I liked reading details such as how a binary integer is displayed in decimal as a “pictured string” based on a series of repeating steps much like the way we are taught to do long division. I liked how different sections of memory were laid out, such as the “pad” where you construct strings, and the “dictionary” where functions are defined, and the “stack” where simple integer math operates. It was possible to grasp the entire pathway from hardware processor architecture through to programming environment.

Taking the example of the green-screen ascii terminal mentioned above, we can describe the process of going from binary bits in memory to the glowing screen.

The screen is 80 columns by 24 lines.
To store this in ascii requires 1920 bytes - just under 2K of RAM.
There are 1280 pixels across the screen, and 384 pixels down.

Each displayable ascii character is encoded as a 16 by 16 grid, where some bits are 0 and some are 1, which represent pixels to be displayed. These bit-maps are stored in read-only memory, and each character requires 32 bytes (two bytes per line).
To access the bitmap of an ascii character requires reading the ascii byte value from memory, multiply by 32, and adding some base offset to get to the start of the character map. This offset might be 0 if the display circuit has dedicated ROM for the character map. The character map might also be RAM, if custom character sets are allowed.

The CRT beam scans from left to right, starting at the top and working it’s way down, line by line. At the end of each line it jumps back to the start of the next line (flyback). To make a bright dot we need to have a 1 which turns on the electron beam, and if there is no dot, the beam is turned off (0).

Now it is simple maths and memory access.
There is a clock signal synced to the CRT refresh and beam tracking, and this clock drives the counters for pixel lines and columns (px and py).
The pixel line count (px) starts at 0 and counts up to 384.
The pixel column (py) is 0 and counts up to 1280.
To get the character position
c = ((1280 * px) + py )/16
Using c, you can peek into the screen RAM to get the ascii value represented at that point of the screen. Then that ascii value and px/py are used to look into the character map ROM to see if the CRT beam position corresponds to a 0 or a 1, and to turn the beam on or off to display a pixel or not.

Of course, modern displays are very different to a green-screen CRT, but the principle is similar. This description is based on the display circuit in my UK-101 (circa 1980), with dedicated display RAM chips. My earlier ZX-81 was much the same, but used additional tricks so that the displayed lines of characters could be terminated, thus reducing the memory requirements to under 1Kb (essential as the base ZX-81 only had 1Kb RAM), which severely limited the amount of the screen that could be populated with characters. To fit a Space Invaders game written in Z80 assembler into 1Kb required drawing characters on half the available lines at a time.

TV Typewriter Cookbook

Get that book and see if you can make your way through it. If not just consider it magic, but it you get a little help you could start understanding what’s going on behind the curtain.

I think you have both of your widths doubled, there. Monitors that were 1280 pixels wide didn’t come about until long, long after green monochrome had been phased out, and characters were taller than they were wide.

Basic 80x24 serial terminal used 7x9 pixel characters. One pixel space between characters. So 80x8 = 640 pixels wide. One pixel space between lines meant 24x10 = 240 pixels deep. Why 80 wide? Because the old programming cards were 80 charters wide. 240 pixels high isn’t always right either. Pixels could be two scan lines high, so often 480 scan lines used. This depended on the monitor. Some terminals were 25 lines high, with the last line used for various status indications. 25x2x10 = 500 scan lines, which is close enough that common TV based CRT components could be used. 640 by 480, with square pixels, became a common early graphics terminal. Drawing graphics could be a awful as creating custom characters, each in an 8x10 block of pixels, and switching character sets, then just drawing them on the screen in the right places. You could do useful line graphics.
Also note that 64 is a power of two, and 48 is a half power. These things mattered to hardware designers as it fitted well with crafting the logic.
Really powerful graphics systems existed even in the 70’s, but cost more than a house. Evans and Sutherland would sell you a graphics monitor with a colour 1000x1000 pixel display, and a refrigerator sized system to draw stuff on it.
If you wanted a computer that you would recognise as a modern system in the 70’s, one with a mouse, a graphics display that used things like separate windows, WYSIWYG text editors with multiple fonts and proportional spacing etc etc, you went to Zerox and bought a Dorado, Dandelion, or other D machine, running Mesa or Smalltalk. You wouldn’t get much change from a house there either. They tried to sell these machines commercially for a while, but were just too far ahead of their time.

If the OP is willing to read a book, I could recommend:

Thank you! I might check this out later next time I go to my library.

Oh? Which languages don’t support logical operations?
The LGP-21 I used in high school put the character you typed on the console into the Accumulator, using a non-ASCII coding. (It was from before ASCII). I did a logic lab which required you to light up a single digit display by presenting it with the right bits in hardware.
When you’re doing microprocessor design, it’s all binary. Everything else is interpretation. Looking at the results of a simulation, you display in hex for convenience but it is seldom the case that displaying the state of the bus as ASCII characters is useful.

Never heard of it but the author, Don Lancaster sounded familiar. On perusing Amazon I came up with
Micro Cookbook
CMOS Cookbook
TTL Cookbook
RTL Cookbook

Aha! Am I showing my age?

You are correct. I’m not sure how I managed to do that, but time passes. I do remember that the ZX-80/81 did not actually use ASCII characters, but only displayed lower-case letters, or their inverse. Also in the set were characters that formed all possible 2x2 pixel blocks, allowing 64x64 pixel graphics (if you hade enough memory to populate the entire screen). Developers wrote graphics games at 64x64 resolution in black and white, and we played them.

I coded a 2D streaming star-field style game in Z80 assembly using half the screen, and later ported it to 6502 on the UK101, which had dedicated video memory allowing full screen. Good times.

All (sane) languages support logical operations, but most don’t support them on a bit level. If you’re just referring to truth values as being bits, well, that can be true, but some languages literally use 32 bits just to store a Boolean true-or-false.

All possible 2x2 blocks, or just half of them (with inverses to get the other half)?

The most non-technical way that I can think of to explain it is that it’s like a blueprint.

A blueprint is just ink on flat paper and yet, somehow, at the end of the day, that all ends up becoming a house and - given a sufficiently precise and detailed blueprint - you could build the exact same house more than once and achieve the exact same result every time.

Why is one particular line a wall, another a door, and yet another a sink? It’s all just ink on paper.

But, the viewer of the blueprint knows some standards in the industry and can be relied on to interpret things in a certain way. Because of those things, the viewer has innate abilities to read more out of the ink than you might think possible if you were an alien species who looked at ink and paper the way that you would look at a list of 1s and 0s.

To get a bit more practical about this, let’s set you up to behave like a computer.

Imagine that we have written a series of digits (0-9) one each into each square on a piece of graph paper. Look at the first (top-left) digit on the page. This gives us a variable type identifier.

If the digit is 0 then a piece of text follows.
If the digit is 1 then a number follows.
Any other values are invalid and are reserved for future use.

If it is a piece of text then the next 4 digits give the length of the text. “0012” would mean that the following 12 digits will be the text. Every set of two digits should be interpreted as a letter of the alphabet. 0-26 are the lowercase letters, 27-52 are the uppercase letters, 53 is a space, 54 is a period, 56 is a comma, 57 is an apostrophe, and all other numbers from 58 to 99 are invalid and reserved for future use.

If it’s a number than the next 10 digits are that number.

Immediately following any number or text, there will either be a new type identifier (like the first digit on the page, telling us of a variable type) or the information will end.

Now, given the above rules, we could create a piece of graph paper that is perfectly valid, contains a variety of numbers and text, and which could be used for some particular purpose.

If you were to simply splash down numbers on the page, willy-nilly, then you would just get a bunch of nonsense when you apply the interpretation rules to it. But, likewise, if you were to splash ink randomly on a piece of paper then no one would be able to build a house off of it.

A computer can use 0s and 1s because there are rules of interpretation and because programmers have carefully written the 0s and 1s into the memory of the computer, with knowledge of how the computer will interpret them. (We don’t actually write 0s and 1s directly, any more, but that is the functional output of our work.)

That is entirely 0. Every language I have ever used (except maybe HP2000 BASIC, it has been so long I forget) has your fundamental & | ^ (and/or/xor) operators along with bit skew operators that let you access individual bits/fields within a (int-type) variable. Additionally, many of them provide for data structure bit field definitions. Realistically, many high level languages supply enough binary functionality that you could write a memory-mapped-port device controller without ever having to resort to assembly/machine coding.

I wrote:

Not the same as not having logical operators. What I was referring to was actually the inability to directly manipulate or create pointers, Which is more common. I wrote the above in haste. But the principle is wider than that.

Logical operations are different from bitwise operations, and all the languages I’ve ever used support bitwise operations. I taught data structures so I’m probably more sensitive to this kind of distinction than most people.

They are often essential. In fault simulation you pack the value of a line of n/2 faulty machines into the n bits of a word. (You have to represent the X value also.) And the biggest issue I had in getting the Zurich Pascal compiler to compile itself on a Multics system was sets which were stores as bits in the standard 60 bit word. Standard according to Jensen and Wirth at least.

Lots of good answers above. I’ll just make a more general point.

There’s no way to understand computers except as a stack of layers. No one on Earth can understand the behavior of even the simplest computer all at once. It’s only possible to understand a layer at a time, and how it interacts with other layers.

At the lowest level, you have transistors (actually, there are even lower levels, but I have to stop somewhere). Transistors are just electronic switches.

At the next level up, you have gates. Gates are made from several transistors and implement a logical operation like AND, OR, NOT, NAND, etc. Importantly, once we have gates we don’t have to think about transistors. The details are hidden, or “abstracted away” as it’s often said. Logical operations are easier to think about than the behavior of transistors.

At the next level up, we assemble gates into various components like binary adders. Put several gates together in the right way and you can add two 8-bit numbers together. Work a bit harder and you multiply two numbers. There are lots of possible components here, from memories to math operations to more specialized things. And importantly, once we’ve built an adder, we no longer have to think much about individual gates–they’re hidden away at the component level (we might still use individual gates at times, but we can stop thinking of components as being built from them).

You can continue on in this way with higher and higher level components. Eventually you stop thinking of hardware at all, and just in terms of the binary code that is being processed (of course, the binary code is stored in a memory somewhere, and then routed through various components, which are themselves built of gates, which are made from transistors).

We keep layering. We don’t think of binary code, we think of a human-readable version of that code (simple instructions like “add” or “jump”). Another layer: don’t think about low-level instructions, think about high-level constructs like “x = (a * b) + (c * d)” that get translated automatically to those instructions.

Each layer is simple enough to understand individually. It might be a semester or two at a university. Learn about enough layers and you have a full understanding of how a computer works. However, you still don’t (and never will) have the capability of understanding the whole thing at once. It’s just too much. The best anyone can do is learning things in a way that the low-level details get fuzzed out and you can kinda ignore them.

And yet at the end, a cosmic ray can hit a transistor, cause a bit flip, and crash your web browser. The lowest level details were not perfectly hidden after all. But it’s pretty close, most of the time.