I have an instruction written in Intel syntax (using gas as my assembler) that looks like this:
mov rdx, msg_size
...
msg: .ascii "Hello, world!\n"
.set msg_size, . - msg
but that mov instruction is being assembled to mov 0xe,%rdx
, rather than mov $0xe,%rdx
, as I would expect. How should I write the first instruction (or the definition of msg_size
) to get the expected behavior?
Use mov edx, OFFSET symbol
to get the symbol "address" as an immediate, rather than loading from it as an address. This works for actual label addresses as well as symbols you set to an integer with .set
.
For the msg
address (not msg_size
assemble-time constant) in 64-bit code, you may wantlea rdx, [RIP+msg]
for a PIE executable where static addresses don't fit in 32 bits. How to load address of function or label into register
In GAS .intel_syntax noprefix
mode:
OFFSET symbol
works like AT&T $symbol
. This is somewhat like MASM.symbol
works like AT&T symbol
(i.e. a dereference) for unknown symbols.[symbol]
is always an effective-address, never an immediate, in GAS and NASM/YASM. LEA
doesn't load from the address but it still uses the memory-operand machine encoding. (That's why lea uses the same syntax).symbol
depends on order of declarationGAS is a one-pass assembler (which goes back and fills in symbol values once they're known).
It decides on the opcode and encoding for mov rdx, symbol
when it first encounters that line. An earlier msize= . - msg
or .equ
/ .set
will make it choose mov reg, imm32
, but a later directive won't be visible yet.
The default assumption for not-yet-defined symbols is that symbol
is an address in some section (like you get from defining it with a label like symbol:
, or from .set symbol, .
). And because GAS .intel_syntax
is like MASM not NASM, a bare symbol is treated like [symbol]
- a memory operand.
If you put a .set
or msg_length=msg_end - msg
directive at the top of your file, before the instructions that reference it, they would assemble to mov reg, imm32
mov-immediate. (Unlike in AT&T syntax where you always need a $
for an immediate even for numeric literals like 1234
.)
For example: source and disassembly interleaved with objdump -dS
:
Assembled with gcc -g -c foo.s
and disassembled with objdump -drwC -S -Mintel foo.o
(with as --version
= GNU assembler (GNU Binutils) 2.34). We get this:
0000000000000000 <l1>:
.intel_syntax noprefix
l1:
mov eax, OFFSET equsym
0: b8 01 00 00 00 mov eax,0x1
mov eax, equsym #### treated as a load
5: 8b 04 25 01 00 00 00 mov eax,DWORD PTR ds:0x1
mov rax, big #### 32-bit sign-extended absolute load address, even though the constant was unsigned positive
c: 48 8b 04 25 aa aa aa aa mov rax,QWORD PTR ds:0xffffffffaaaaaaaa
mov rdi, OFFSET label
14: 48 c7 c7 00 00 00 00 mov rdi,0x0 17: R_X86_64_32S .text+0x1b
000000000000001b <label>:
label:
nop
1b: 90 nop
.equ equsym, . - label # equsym = 1
big = 0xaaaaaaaa
mov eax, OFFSET equsym
1c: b8 01 00 00 00 mov eax,0x1
mov eax, equsym #### treated as an immediate
21: b8 01 00 00 00 mov eax,0x1
mov rax, big #### constant doesn't fit in 32-bit sign extended, assembler can see it when picking encoding so it picks movabs imm64
26: 48 b8 aa aa aa aa 00 00 00 00 movabs rax,0xaaaaaaaa
It's always safe to use mov edx, OFFSET msg_size
to treat any symbol (or even a numeric literal) as an immediate regardless of how it was defined. So it's exactly like AT&T $
except that it's optional when GAS already knows the symbol value is just a number, not an address in some section. For consistency it's probably a good idea to always use OFFSET msg_size
so your code doesn't change meaning if some future programmer moves code around so the data section and related directives are no longer first. (Including future you who's forgotten these strange details that are unlike most assemblers.)
BTW, .set
is a synonym for .equ
, and there's also symbol=value
syntax for setting a value which is also synonymous to .set
.
mov rdx, OFFSET symbol
will assemble to mov r/m64, sign_extended_imm32
. You don't want that for a small length (vastly less than 4GiB) unless it's a negative constant, not an address. You also don't want movabs r64, imm64
for addresses; that's inefficient.
It's safe under GNU/Linux to write mov edx, OFFSET symbol
in a position-dependent executable, and in fact you should always do that or use lea rdx, [rip + symbol]
, never sign-extended 32-bit immediate unless you're writing code that will be loaded into the high 2GB of virtual address space (e.g. a kernel). How to load address of function or label into register
See also 32-bit absolute addresses no longer allowed in x86-64 Linux? for more about PIE executables being the default in modern distros.
Tip: if you know the AT&T or NASM syntax, or the NASM syntax, for something, use that to produce the encoding you want and then disassemble with objdump -Mintel
to find out the right syntax for .intel_syntax noprefx
.
But that doesn't help here because disassembly will just show the numeric literal like mov edx, 123
, not mov edx, OFFSET name_not_in_object_file
. Looking at gcc -masm=intel
compiler output can also help, but again compilers do their own constant-propagation instead of using symbols for assemble-time constants.
BTW, no open-source projects that I'm aware of contain GAS intel_syntax source code. If they use gas, they use AT&T syntax. Otherwise they use NASM/YASM. (You sometimes also see MSVC inline asm in open source projects).
[RIP + symbol]
This is a lot more artificial since you wouldn't normally do this with an integer constant that wasn't an address. I include it here just to show another facet of GAS's behaviour depending on a symbol being defined or not at a point during its 1 pass.
How do RIP-relative variable references like "[RIP + _a]" in x86-64 GAS Intel-syntax work? - [RIP + symbol]
is interpreted as using relative addressing to reach symbol
, not actually adding two addresses. But [RIP + 4]
is taken literally, as an offset relative to the end of this instruction.
So again, it matters what GAS knows about a symbol when it reaches an instruction that references it, because it's 1-pass. If undefined, it assumes it's a normal symbol. If defined as a numeric value with no section associated, it works like a literal number.
_start:
foo=4
jmpq *foo(%rip)
jmpq *bar(%rip)
bar=4
That assembles to the first jump being the same as jmp *4(%rip)
loading a pointer from 4 bytes past the end of the current instruction. But the 2nd jump using a symbol relocation for bar
, using a RIP-relative addressing mode to reach the absolute address of the symbol bar
, whatever that may turn out to be.
0000000000000000 <.text>:
0: ff 25 04 00 00 00 jmp QWORD PTR [rip+0x4] # a <.text+0xa>
6: ff 25 00 00 00 00 jmp QWORD PTR [rip+0x0] # c <bar+0x8> 8: R_X86_64_PC32 *ABS*
After linking with ld foo.o
, the executable has:
401000: ff 25 04 00 00 00 jmp *0x4(%rip) # 40100a <bar+0x401006>
401006: ff 25 f8 ef bf ff jmp *-0x401008(%rip) # 4 <bar>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With