0

UPDATE: I solved this problem with the help of Mark Tolonen's answer below. Here is the solution (but I'm puzzled by one thing):

I begin with the encoding string shown in Mark Tolonen's answer below (UTF-8):

CA_f1 = (ctypes.c_char_p * len(f1))(*(name.encode() for name in f1))

With optimizations off, I always store rcx into a memory variable on entry. Later in the program when I need to use the pointer in rcx, I read it from memory. That works for a single pointer, but doesn't work for accessing the pointer array Mark Tolonen showed below; maybe that's because it's a pointer array, not just a single pointer. It DOES work if I store rcx into r15 on entry, and downstream in the program it works like this:

;To access the first char of the first name pair: 

xor rax,rax
mov rdx,qword[r15]
movsx eax,BYTE[rdx]
ret

;To access the second char of the second name pair: 

mov rdx,qword[r15+8]
movsx eax,BYTE[rdx+1]

That's not a problem because I usually store as many variables as possible in registers; sometimes there are not enough registers, so I have to resort to storing some in memory. Now, when processing strings, I will always reserve r15 to hold the pointer passed in rcx if it's a pointer array.

Any insight into why the memory location doesn't work?

**** END OF ANSWER ****

I'm new to string processing in NASM, and I am passing a string from ctypes. The string data is read from a text file (Windows .txt), using the following Python function:

with open(fname, encoding = "utf8") as f1:
        for item in f1:
            item = item.lstrip()
            item = item.rstrip()
            return_data.append(item)
    return return_data

The .txt file contains a list of first and last names, separated by newline-linefeed characters.

I pass a c_char_p pointer to a NASM dll using ctypes. The pointer is created with this:

CA_f1 = (ctypes.c_char_p * len(f1))()

Visual Studio confirms that it is a pointer to a byte string 50 NAMES long, which is where the problem may be, I need bytes, not list elements. Then I pass it using this ctypes syntax:

CallName.argtypes = [ctypes.POINTER(ctypes.c_char_p),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double)]

UPDATE: before passing the string, now I convert the list to a string like this:

f1_x = ' '.join(f1)

Now VS shows a pointer to a 558 byte string, which is correct, but I still can't read a byte.

In my NASM program, I test it by reading a random byte into al using the following code:

lea rdi,[rel f1_ptr]
mov rbp,qword [rdi] ; Pointer
xor rax,rax
mov al,byte[rbp+1]

But the return value in rax is 0.

If I create a local string buffer like this:

name_array: db "Margaret Swanson"

I can read it this way:

mov rdi,name_array
xor rax,rax
mov al,[rdi]

But not from a pointer passed into a dll.

Here's the full code for a simple, reproducible example in NASM. Before passing it to NASM, I checked random bytes and they are what I expect, so I don't think it's encoding.

[BITS 64]
[default rel]

extern malloc, calloc, realloc, free
global Main_Entry_fn
export Main_Entry_fn
global FreeMem_fn
export FreeMem_fn

section .data align=16
f1_ptr: dq 0
f1_length: dq 0
f2_ptr: dq 0
f2_length: dq 0
data_master_ptr: dq 0

section .text

String_Test_fn:
;______

lea rdi,[rel f1_ptr]
mov rbp,qword [rdi]
xor rax,rax
mov al,byte[rbp+10]
ret

;__________
;Free the memory

FreeMem_fn:
sub rsp,40
call free
add rsp,40
ret

; __________
; Main Entry

Main_Entry_fn:
push rdi
push rbp
mov [f1_ptr],rcx
mov [f2_ptr],rdx

mov [data_master_ptr],r8
lea rdi,[data_master_ptr]
mov rbp,[rdi]
xor rcx,rcx
movsd xmm0,qword[rbp+rcx]
cvttsd2si rax,xmm0
mov [f1_length],rax
add rcx,8
movsd xmm0,qword[rbp+rcx]
cvttsd2si rax,xmm0
mov [f2_length],rax
add rcx,8

call String_Test_fn

pop rbp
pop rdi
ret

UPDATE 2:

In reply to a request, here is a ctypes wrapper to use:

def Read_Data():

    Dir= "[FULL PATH TO DATA]"

    fname1 = Dir + "Random Names.txt"
    fname2 = Dir + "Random Phone Numbers.txt"

    f1 = Trans_02_Data.StrDataRead(fname1)
    f2 = Trans_02_Data.StrDataRead(fname2)
    f2_Int = [  int(numeric_string) for numeric_string in f2]
    StringTest_asm(f1, f2_Int)

def StringTest_asm(f1,f2):

    f1.append("0")

    f1_x = ' '.join(f1)
    f1_x[0].encode(encoding='UTF-8',errors='strict')

    Input_Length_Array = []
    Input_Length_Array.append(len(f1))
    Input_Length_Array.append(len(f2*8))

    length_array_out = (ctypes.c_double * len(Input_Length_Array))(*Input_Length_Array)

    CA_f1 = (ctypes.c_char_p * len(f1_x))() #due to SO research
    CA_f2 = (ctypes.c_double * len(f2))(*f2)
    hDLL = ctypes.WinDLL("C:/NASM_Test_Projects/StringTest/StringTest.dll")
    CallName = hDLL.Main_Entry_fn
    CallName.argtypes = [ctypes.POINTER(ctypes.c_char_p),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double)]
    CallName.restype = ctypes.c_int64

    Free_Mem = hDLL.FreeMem_fn
    Free_Mem.argtypes = [ctypes.POINTER(ctypes.c_double)]
    Free_Mem.restype = ctypes.c_int64
    start_time = timeit.default_timer()

    ret_ptr = CallName(CA_f1,CA_f2,length_array_out)

    abc = 1 #Check the value of the ret_ptr, should be non-zero   
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
RTC222
  • 2,025
  • 1
  • 20
  • 53
  • Please post the *Python* code, not just pieces. Also where is *CallName* defined (in the *.dll*)? – CristiFati Jan 22 '19 at 19:13
  • I'm not 100% sure of the syntax, but it seems like your `lea` is computing the address of `f1_ptr`. Then you use `[rdi]` which seems to treat `f1_ptr` as a pointer itself. (So, two levels of pointer.) Is this correct, and is it what you intend? – aghast Jan 22 '19 at 19:15
  • Austin, f1_ptr is just a variable name. I assign it to rdi and then to rbp, which is how I handle data arrays. I'm not sure if that's correct for byte strings. CristiFati, I'll post the full ctypes code shortly. Thanks for the replies. – RTC222 Jan 22 '19 at 19:32
  • CristiFati, CallName is a Python ctypes convention. There is no corresponding name in the dll. The dll is basically independent of Python. – RTC222 Jan 22 '19 at 19:42
  • It's no convention, it's just how you named a variable in your code. So what's it supposed to do ? (sorry my asm knowledge is very low). You are aware that the 1st argument is a `char*` array that contains only *NULL*s? Also you seem to mess all the strings: *f1\_x * is a string that contain all the names separated by spaces. I really don't see where `ctypes.POINTER(ctypes.c_char_p)` would fit in tis equation. Also `f1_x[0].encode...` is totally useless (and probably wrong). – CristiFati Jan 22 '19 at 20:22
  • Also I downloaded *nasm 2.14.02*, and when building your file I get: "*dll.asm:6: error: parser: instruction expected*" – CristiFati Jan 22 '19 at 20:40
  • ChrisiFati, thanks for your comments. You're right that f1_x[0].encode serves no greater purpose, it was just a test I did and should not be in the code above (removed). ctypes.POINTER(ctypes.c_char_p) passes a pointer to a char array. As your asm knowledge is low, you may not understand the problem best because the issue seems to be NASM-specific, but I'll check your comment that the char array contains only NULLS -- what data are you using for that? – RTC222 Jan 22 '19 at 20:55
  • I just checked the data I have and they are definitely not NULLS. I'm curious what you meant? – RTC222 Jan 22 '19 at 20:56
  • `CA_f1 = (ctypes.c_char_p * len(f1_x))()`. How do you build your *asm* code? – CristiFati Jan 22 '19 at 21:24
  • Build it using the NASM compiler and a linker. I use GoLink for Windows; can also use minGW64 for Windows. – RTC222 Jan 22 '19 at 21:27
  • If you posted this on code-review, there's a *lot* of inefficiency here. Like `FreeMem_fn` probably doesn't need to be a wrapper at all, if you can get NASM or the linker to create a symbol that's an alias for `free`, i.e. has the same address. Or if you are doing to write a wrapper, you can just taillcall, like `jmp free` instead of sub/call/add/ret. The double->int conversion also looks inefficient; you could do that with [`cvttpd2dq xmm0, [rbp]`](https://www.felixcloutier.com/x86/cvttpd2dq) if the data is aligned, otherwise load it with `movups` first. Definitely don't `add rcx,8`. – Peter Cordes Jan 24 '19 at 21:22
  • You don't show anything using the lengths, but if you do need full 64-bit double->int64_t results, then yeah you need to use scalar unless you have AVX512. But still use `[rbp+8]` instead of messing around with `add rcx,8`. You only need about 2 instructions, like `cvttsd2si rax, [r8]` / `cvttsd2si rdx, [r8 + 8]`, without any of the store/reload of the pointer or other nonsense. – Peter Cordes Jan 24 '19 at 21:26

1 Answers1

3

Your name-reading code would return a list of Unicode strings. The following would encode a list of Unicode strings into an array of strings to be passed to a function taking a POINTER(c_char_p):

>>> import ctypes
>>> names = ['Mark','John','Craig']
>>> ca = (ctypes.c_char_p * len(names))(*(name.encode() for name in names))
>>> ca
<__main__.c_char_p_Array_3 object at 0x000001DB7CF5F6C8>
>>> ca[0]
b'Mark'
>>> ca[1]
b'John'
>>> ca[2]
b'Craig'

If ca is passed to your function as the first parameter, the address of that array would be in rcx per x64 calling convention. The following C code and its disassembly shows how the VS2017 Microsoft compiler reads it:

DLL code (test.c)

#define API __declspec(dllexport)

int API func(const char** instr)
{
    return (instr[0][0] << 16) + (instr[1][0] << 8) + instr[2][0];
}

Disassembly (compiled optimized to keep short, my comments added)

; Listing generated by Microsoft (R) Optimizing Compiler Version 19.00.24215.1

include listing.inc

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC  func
; Function compile flags: /Ogtpy
; File c:\test.c
_TEXT   SEGMENT
instr$ = 8
func    PROC

; 5    :     return (instr[0][0] << 16) + (instr[1][0] << 8) + instr[2][0];

  00000 48 8b 51 08      mov     rdx, QWORD PTR [rcx+8]  ; address of 2nd string
  00004 48 8b 01         mov     rax, QWORD PTR [rcx]    ; address of 1st string
  00007 48 8b 49 10      mov     rcx, QWORD PTR [rcx+16] ; address of 3rd string
  0000b 44 0f be 02      movsx   r8d, BYTE PTR [rdx]     ; 1st char of 2nd string, r8d=4a
  0000f 0f be 00         movsx   eax, BYTE PTR [rax]     ; 1st char of 1st string, eax=4d
  00012 0f be 11         movsx   edx, BYTE PTR [rcx]     ; 1st char of 3rd string, edx=43
  00015 c1 e0 08         shl     eax, 8                  ; eax=4d00
  00018 41 03 c0         add     eax, r8d                ; eax=4d4a
  0001b c1 e0 08         shl     eax, 8                  ; eax=4d4a00
  0001e 03 c2            add     eax, edx                ; eax=4d4a43

; 6    : }

  00020 c3               ret     0
func    ENDP
_TEXT   ENDS
END

Python code (test.py)

from ctypes import *

dll = CDLL('test')
dll.func.argtypes = POINTER(c_char_p),
dll.restype = c_int

names = ['Mark','John','Craig']
ca = (c_char_p * len(names))(*(name.encode() for name in names))
print(hex(dll.func(ca)))

Output:

0x4d4a43

That's the correct ASCII codes for 'M', 'J', and 'C'.

Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251
  • Working from your example, using NASM, I still have problems. Your encoding example returns an array of pointers to each of the names (first-last pairs), as you said. I store rcx into a variable (f1_ptr) on entry. To check the ascii value of the first letter in the first name ("H") the closest I get is: xor rax,rax,mov rdx,qword[f1_ptr],mov al,byte[rdx],ret. But it doesn't return the correct ascii value. Separate coding for NASM? How would I take the second character in the first name ("a")? Using movsx, as in your example, returns what looks like a pointer value. – RTC222 Jan 23 '19 at 20:00
  • On further inspection, it's returning the low byte of a pointer value, not what it points to. – RTC222 Jan 23 '19 at 20:24
  • Mark, with the help of you answer I solved this problem, and I posted the solution at the top of my question above. Any idea why the memory location doesn't work? – RTC222 Jan 23 '19 at 22:21
  • @RTC222 You should post another question about how to store and retrieve in assembly. It's a different problem than the original question. Show your current code to store as well as retrieve that isn't working. – Mark Tolonen Jan 24 '19 at 05:14
  • @RTC222 Remember that `rcx` is the address of an array of pointers. Dereference one more time. You've loaded the array address into `rdx`, but then read a byte...you need to read the address at `rdx` (for first string) or `rdx+8` (for second), etc. *then* read a byte. – Mark Tolonen Jan 24 '19 at 05:36