Classic AVRs (like the ATmega328p, and unlike the ATmega4809) can only use the main clock source selected by the fusebits.
For classic AVRs, this is configured by the "low fuse" (there are 3 on classic AVRs, low, high, and - except on very old parts - extended), and cannot be changed at runtime (with a couple of exotic exceptions). This low fuse is set by avrdude when you do "burn bootloader". I reckon that you probably mean crystal, not external clock (there's only one kind of external clock, and it requires a full on external oscillator, not just a crystal - this would be either another microcontroller's CKOUT/CLKO pin, or a standalone device with power, ground, and clock output pins), since low power oscillator is a crystal option, not external clock. Setting clock source to external clock when all you have is a crystal will soft-brick your chip.
External crystal (I think low power - though it might be full swing; full swing is generally more reliable, especially with non-ideal layout around the crystal) is what Uno is configured for by default, so if you burn bootloader as uno, you are all set.
Modern AVRs, in contrast, always start from internal and application code sets the clock source (my cores, and I assume MegaCoreX for the ATmega 0-series handle this in init() which runs before setup() - but it wasn't until the DB-series released this summer, that they even had external high frequency crystal support!). What you described as assembly is just C - only low level C that is manipulating registers directly. That looks like what would be used for the exotic classic AVRs that let you do that (of which the ATmega328(p) is not one).
If that's an actual ATmega328 not ATmega328p, bootloading will fail and complain of signature mismatch (because the uno tells avrdude to look for a '328p, not a '328). Install MiniCore through boards manager ( github.com/MCUDude/MiniCore) and bootload at 16MHz 328 and you're good to go.