First you write the program out in assembly language instructions then you convert individual instructions and keep track of the program counter on the left hand side, you will have to leave space for the operands of forward jumps because you don't know what that address will be be. Then you go back and fill them in. Easy-peasy.
There is a nice image of such a notebook here
https://www.quora.com/What-is-the-real-purpose-of-graph-pape...
which looks exactly like the listing output of an assembler because it is.
In any case, none of this was particularly difficult or unusual, just a bit tedious. At least, the part where you enter hex codes is.
Even more fun was before microcomputers. On a few minicomputers and mainframes, you would occasionally have to enter short programs in binary using switches on the front panel.
After 6 years of being a field engineer and writing exclusively in machine language, I made the switch to being a systems analyst which gave me access to an assembler. That seemed easy after having to calculate relative jumps/subroutine calls for so long.
I would use a hand punch to create holes in paper tape that was the program. (Just a metal block with holes drilled and a metal dowel to punch the tape).
"Patching the tape" involved finding a section of code that could be "overpunched" to be a jump instruction to the current end of tape where you could punch in a subroutine and a return statement.
If all else failed you'd create a "patch tape" that was read after the original tape and overwrote memory locations.
It was all very convenient since you could "single step" the program from the front panel and watch the registers change.
Life got really sweet when the new teletype had a reader/punch.
Good times.