8
votes

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?

1
when I try that, I get undefined reference to `$msg_size'Alex Reinking
Oh, sorry, I missed the part of about Intel syntax. In true MASM syntax you wouldn't need to do anything. You could try OFFSET msg_sizeRoss Ridge
Yes, that works, thank you. I'm a bit too used to nasm, I think...Alex Reinking
@RossRidge Sorry, one more question. What works similarly in lea <reg>, [<reg> + <constant>]?Alex Reinking
In that context it shouldn't matter. It's unambiguously a displacement.Ross Ridge

1 Answers

11
votes

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 want
lea 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 GNU Assembler


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).

Interpretation of bare symbol depends on order of declaration

GAS 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.


Operand-size: generally use 32-bit unless a value needs 64

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 in GNU Assembler

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).