I am an electronics student and I am learning how to use assembly language on an ARM Cortex-M4, specifically for the STM32F401CCUx microcontroller and, even more specifically, on the BlackPill board (I am using Keil uVision, the current version as of the date of this post) . While working on my project initializing and manipulating a 16x2 LCD, I wondered if ever in history have multiple assembler files been used for a single project and how they communicate with each other to maintain greater order in the project. So far, I have tried and failed. I want to make use of only .s files. Have a file which I can call within my main code solely for initialization and, in my main, send the data. Tried the Export, Import, and Get directives, "BL LR" and "BX LR" but was unsuccessful. I would like to know if any reader could guide me to achieve my goal. My goal is to learn and use assembler like a professional... Hehehe
-
\$\begingroup\$ if ever in history have multiple assembler files been used for a single project: This was most common in history, at least since I program (the last 40+ years). :-D \$\endgroup\$the busybee– the busybee2024年06月06日 05:30:09 +00:00Commented Jun 6, 2024 at 5:30
-
\$\begingroup\$ Pinula1, is there something more you need to know? \$\endgroup\$periblepsis– periblepsis2024年06月11日 09:49:07 +00:00Commented Jun 11, 2024 at 9:49
2 Answers 2
I can't speak to Keil uVision. It's been many years since I used Keil. But I can talk, generally.
An assembler usually produces an object file
. This is a custom formatted file that typically is composed of a series of records that contain symbols, strings of code bytes, strings of initialized data, and section information. (A lot more than that, but that's a quick summary.) An object file
is not particularly useful until after a linker processes it. But it is very useful to the linker.
(It's possible -- and some do this -- for an assembler to directly generate binary code, which may either be directly usable by a loader program or else can be directly written to ROM. But this isn't possible if there is more than one source file being assembled one at a time by the assembler. And that's the case you are asking about.)
The assembler can be applied to source files, one after another, until all of the source files have been turned into object files
. Then, usually, the linker is given the names of all these object files
, along with a linker input file
that adds the remaining information needed to allocate memory from the right places and then build a file that is compatible with the loader program or used to place data into a ROM. The linker input file (or the command line) may also specify default library code and data to use, if necessary.
Symbols in an object file
can have global or local file scope. If local file scope, then the linker will only recognize references to that symbol name within the source file it's processing at the time. If global scope, then the linker will match up these names across all of the object files
and consider them to be the same throughout the program unit. The assembler will have syntax to allow either choice and, likely, a default if you don't specify which of the two.
Symbols can either be labels associated with code (for branching instruction targets) or be associated with data memory. The linker is well aware of such differences and won't mix them up.
Also, code and data can be grouped into named-groups and these named-groups can be combined into sections. The sections can be located at specific memory addresses, using the linker input file. How much detailed control over locating code and data (whether or not groups can themselves be located, or sections only) is a matter for the specific linker.
The only way you can wield multi-file projects like a professional is to study other projects and to practice, a lot, writing your own code.
Read up on the build process; in particular, linking. Even more particularly: consult the user manual(s) of the tools (Keil) you're using.
To summarize, every file (module) that is parsed, is transformed into object code, containing the machine code itself, its length, entry points (valid locations to jump to, and any preconditions/cleanup if needed to do so), and locations that record addresses to code/data; and so on for data objects, and other memory spaces as applicable. At the most basic, an object is a named ("symbol") or anonymous section of memory, having an address (base/offset) and length, which the linker uses to allocate (if fixed) or arrange (when offset is TBD) the object code into a complete binary blob. The linker places objects according to device rules (memory spaces, sizes, alignment, etc.), and final address values are filled in until no symbols remain and all addresses are realized.
Hence, one byproduct of the linking process, useful for debug purposes, is the symbol table: the conversion between named objects (variables, functions, etc.) and logical and physical addresses. You can imagine, stepping through live code while debugging, it's helpful to know where you're at, both in code and data spaces; or to unwind the stack to see the call sequence (how much data is allocated on the stack, what functions have been called, etc.).
Using multiple .s
files is as simple as using symbols from other modules as needed, and avoiding stomping over (reusing) symbols already declared elsewhere. I can't speak for Keil personally, but for most build systems, it's as simple as enumerating all sources in the project, then building it (the build tool runs the linker on all object files produced from those sources). .s
may be compiled (many compilers accept ASM natively, e.g. GCC), or compatible object is produced with an assembler per se.