I am currently re-coding some functions from the standard C library in NASM x64. At the moment I only have two functions:
strlen:
bits 64
section .text
global strlen
strlen:
xor rbx, rbx
jmp strlen_loop
strlen_loop:
cmp byte [rdi + rbx], 0
je strlen_ret
inc rbx
jmp strlen_loop
strlen_ret:
mov rax, rbx
ret
putstr:
bits 64
extern strlen
STDOUT equ 1
section .text
global putstr
putstr:
call strlen
mov rdx, rax
xor rax, rax
mov rdi, STDOUT
syscall
ret
Here is the makefile:
CC = nasm
CFLAGS = -f elf64
SRC = $(wildcard src/*.asm)
OBJ = $(SRC:.asm=.o)
NAME = minilibc.so
all: $(NAME)
$(NAME): $(OBJ)
ld -fPIC -shared -o $(NAME) $(OBJ) -nostdlib
%.o: %.asm
$(CC) $(CFLAGS) $< -o $@
clean:
rm -f $(OBJ)
fclean: clean
rm -f $(NAME)
re: fclean all
test: re
gcc -o test/test test/main.c
LD_PRELOAD=./minilibc.so ./test/test
.PHONY: all clean fclean re
and the test code:
#include <stdio.h>
extern size_t putstr(const char *str);
int main(void)
{
const char *str = "Hello, World!\n";
putstr(str);
return (0);
}
Here's the error I get:
rm -f src/putstr.o src/strlen.o rm -f minilibc.so nasm -f elf64 src/putstr.asm -o src/putstr.o nasm -f elf64 src/strlen.asm -o src/strlen.o ld -fPIC -shared -o minilibc.so src/putstr.o src/strlen.o -nostdlib ld: src/putstr.o: attention: readdress on "strlen" in read-only ".text" section ld: src/putstr.o: readdressing R_X86_64_PC32 to symbol "strlen" cannot be used when creating a shared object; recompile with -fPIC ld: final link edit failed: wrong value make: *** [Makefile:10: minilibc.so] Error 1
LD seems to be telling me to recompile in PIC mode, which I'm already doing, so is the problem that my functions have the same name as the GLIBC functions? However, I'm compiling without the standard library.
Use global strlen:function hidden
instead of global strlen
to make the symbol visible to the linker so other object files can reference it, but not exported from the .so
shared object we're making so it doesn't participate in symbol interposition.
Or to allow efficient calls inside the library but still export the symbol outside, define a second label at the same place, like strlen_internal: strlen:
and make the internal one hidden
but the other one just normal global strlen:function
non-hidden. I don't know of a better way to do this, to get ld
to make calls to a visibility=default symbol only go to the definition inside this shared object.
Your global strlen
makes the symbol fully global, participating in symbol interposition. The linker won't let you reference that symbol with a call rel32
since the actual definition that gets used at dynamic link time might be in a different shared library (for example libc.so
since you used the name of a standard C function), and it might be loaded more than +- 2GiB away from this shared library.
In 32-bit code, a rel32 can reach anywhere in virtual address-space, so "re-addressing" (like a text relocation) could handle it with just the warning about needing to rewrite the machine code in a read-only page (in .text
).
In this case you don't want to let your library call a faster version of strlen
if the dynamic linker has already seen one in a different library. Your putstr
depends on custom behaviour of your strlen
that the ABI doesn't require (e.g. not modifying the RDI pointer arg so you have it available after finding the length, without saving/restoring anything on the stack).
Your strlen
is also not ABI-compatible with compiler-generated code; it modifies the caller's RBX, which is a call-preserved register in standard x86-64 calling conventions. But that's a problem even when it's called from putstr
since that eventually returns to a C caller. Just use RAX in the first place, instead of a separate register that you later copy to the return value. Or count into RDX since this private version of strlen
only exists as a helper for putstr
. (Maybe call it a different name, too.)
Your putstr
is also just plain broken, BTW; it doesn't copy RDI to RSI for the void*
arg to write
, and actually makes a read
system call not write
. (The system call numbers match the standard file descriptor, e.g. __NR_write = STDOUT_FD = 1
in unistd_64.h
for x86-64 Linux, this is a helpful choice for remembering them.) Presumably you'd notice that if you tried this not in a shared library so you could get it working linked into an executable. Or if you put your strlen
in the same .asm
as putstr
and made it not global
.
Related:
https://nasm.us/doc/nasmdoc8.html#section-8.9.5 - elf
Extensions to the GLOBAL
Directive documents this.
Call a function in another object file without using PLT within a shared library? GAS version of the same problem. In GAS, you'd do .globl strlen
and .hidden strlen
. And IIRC, .type strlen, @function
Can't call C standard library function on 64-bit Linux from assembly (yasm) code - three ways to call functions in other shared libraries: PLT, gcc -fno-plt
style, or make a non-PIE executable so the linker invents indirection via the PLT for you. The example at the bottom of linkers "relaxing" calls from indirect/PLT to direct mentions NASM's hidden
flag.
NASM Linux shared object error: Relocation R_X86_64_32S against '.data' also mentions NASM's hidden
flag for the global
directive.