c an assembler

DarkAvenger

Commodore Special
Mitglied seit
20.05.2003
Beiträge
391
Renomée
0
So, ich schreibe mal lieber einen seperaten thread hierzu, weil es eher von allg. Natur ist.

Ich war ja am abwägen, ob ich den gcc inline assembler oder nasm benutzen soll. Ich denke, daß ich doch lieber nasm benutzen sollte, weil mir diese Lösung portabler erscheint.

Also versuche ich nun zu verstehen, wie man unter nasm Prozeduren schreibt, die man von c aus aufrufen kann.

Dabei habe ich folgendes in der nasm Doku gefunden und bin ein wenig verwirrt:

Code:
Thus, you would define a function in C style in the following way:

global  _myfunc 

_myfunc: 
        push    ebp 
        mov     ebp,esp 
        sub     esp,0x40        ; 64 bytes of local stack space 
        mov     ebx,[ebp+8]     ; first parameter to function 

        ; some more code 

        leave                   ; mov esp,ebp / pop ebp 
        ret

Ich dachte mir, wozu den Anfang, dazu gibt es doch ENTER:

B.4.65 ENTER: Create Stack Frame


ENTER imm,imm ; C8 iw ib [186]


ENTER constructs a stack frame for a high-level language procedure call. The first operand (the iw in the opcode definition above refers to the first operand) gives the amount of stack space to allocate for local variables; the second (the ib above) gives the nesting level of the procedure (for languages like Pascal, with nested procedures).


The function of ENTER, with a nesting level of zero, is equivalent to


PUSH EBP ; or PUSH BP in 16 bits
MOV EBP,ESP ; or MOV BP,SP in 16 bits
SUB ESP,operand1 ; or SUB SP,operand1 in 16 bits


This creates a stack frame with the procedure parameters accessible upwards from EBP, and local variables accessible downwards from EBP.


With a nesting level of one, the stack frame created is 4 (or 2) bytes bigger, and the value of the final frame pointer EBP is accessible in memory at [EBP-4].


This allows ENTER, when called with a nesting level of two, to look at the stack frame described by the previous value of EBP, find the frame pointer at offset -4 from that, and push it along with its new frame pointer, so that when a level-two procedure is called from within a level-one procedure, [EBP-4] holds the frame pointer of the most recent level-one procedure call and [EBP-8] holds that of the most recent level-two call. And so on, for nesting levels up to 31.


Stack frames created by ENTER can be destroyed by the LEAVE instruction: see section B.4.136.

Nun weiß ich nicht, ob ich was durcheinander schmeiße, bzw was mit nested oben gemeint ist, und wie ich den Parameter für c setzten müßte. In welcher Realtion dazu steht folg. Passus (insbesondere interessieren mich die verschiedenen offsets.

The callee may then access its parameters relative to EBP. The doubleword at [EBP] holds the previous value of EBP as it was pushed; the next doubleword, at [EBP+4], holds the return address, pushed implicitly by CALL. The parameters start after that, at [EBP+8]. The leftmost parameter of the function, since it was pushed last, is accessible at this offset from EBP; the others follow, at successively greater offsets. Thus, in a function such as printf which takes a variable number of parameters, the pushing of the parameters in reverse order means that the function knows where to find its first parameter, which tells it the number and type of the remaining ones.

Für die Parameter habe ich bei nasm noch folg gefunden:

8.1.4 c32.mac: Helper Macros for the 32-bit C Interface

Included in the NASM archives, in the misc directory, is a file c32.mac of macros. It defines three macros: proc, arg and endproc. These are intended to be used for C-style procedure definitions, and they automate a lot of the work involved in keeping track of the calling convention.

An example of an assembly function using the macro set is given here:

proc _proc32

%$i arg
%$j arg
mov eax,[ebp + %$i]
mov ebx,[ebp + %$j]
add eax,[ebx]

endproc

This defines _proc32 to be a procedure taking two arguments, the first (i) an integer and the second (j) a pointer to an integer. It returns i + *j.

Note that the arg macro has an EQU as the first line of its expansion, and since the label before the macro call gets prepended to the first line of the expanded macro, the EQU works, defining %$i to be an offset from BP. A context-local variable is used, local to the context pushed by the proc macro and popped by the endproc macro, so that the same argument name can be used in later procedures. Of course, you don't have to do that.

arg can take an optional parameter, giving the size of the argument. If no size is given, 4 is assumed, since it is likely that many function parameters will be of type int or pointers.


So, meine Fragen nun:

- kann ich einfach ENTER statt push und mov und sub für den Anfang benutzen? Oder gibt es einen Grund dies nciht zu tun.

- Woher weiß ich, wieviel stack space ich reservieren muß für "lokale Var."? (erster Paramter ENTER.)

- Was soll der 2. Parameter bei ENTER und im Zshng mit c.

- Was hat das mit den ebp offsets (s.o.) in + und in - Richtung auf sich?

- Ich habe ja verstanden, daß der return Wert in eax stehen sollte. Im obigen Bsp mit einer Übergabe als pointer, da reicht es doch (um dem c standrad zu genügen) und die verweiste Speicherstelle upzudaten, wenn ich den Wert, auf den der pointer zeigt, ändere?

- Bzgl der nasm Syntax/Makro arg: Woher weiß ich anhand des Quelltextes, was für ein Datentyp es ist? Sehe ich es nur implizit, was ich damit anstelle? Bzw oben steht einfach "integer", woher weiß nasm ob es byte, word oder dword ist?
 
Ach ja, wo ich schon dabei bin: Jemand ne AHnung, wie ich nasm in kdevelop einbinden kann?
 
So, meine Fragen nun:

- kann ich einfach ENTER statt push und mov und sub für den Anfang benutzen? Oder gibt es einen Grund dies nciht zu tun.

- Woher weiß ich, wieviel stack space ich reservieren muß für "lokale Var."? (erster Paramter ENTER.)

- Was soll der 2. Parameter bei ENTER und im Zshng mit c.

- Was hat das mit den ebp offsets (s.o.) in + und in - Richtung auf sich?

- Ich habe ja verstanden, daß der return Wert in eax stehen sollte. Im obigen Bsp mit einer Übergabe als pointer, da reicht es doch (um dem c standrad zu genügen) und die verweiste Speicherstelle upzudaten, wenn ich den Wert, auf den der pointer zeigt, ändere?

- Bzgl der nasm Syntax/Makro arg: Woher weiß ich anhand des Quelltextes, was für ein Datentyp es ist? Sehe ich es nur implizit, was ich damit anstelle? Bzw oben steht einfach "integer", woher weiß nasm ob es byte, word oder dword ist?

1. das kommt darauf an wie das Stack Frame normalerweise vom C-Compiler angelegt wird. Jeder macht da sein eigenes, ENTER liefert dir den Code für eine bestimmte Variante.
Ob das nun die ist die auch der C-Compiler verlangt weis ich net, hab ENTER bisher noch nie gesehen. Und es spricht auch nix dagegen das nicht zu nehmen, die 10 Byte Code machens dann auch nicht mehr.

2. Wenn du in C in einer Funktion ein "int a" stehen hast, braucht der integer a 4 Byte lokalen Speicher, der normalerweise dann eben vom Stack genommen wird.
Bei selbstgeschriebenen Assemblerroutinen nimmt man idR keine lokalen Variablen, da zu unübersichtlich nur mit dem Offsets zu arbeiten (die hast du dann eben in der Form [EBP+4] oder so).

3. ENTER ist so wie ich das mitbekommen hab doch ein x86 Assembler Befehl (oder? - hab den ehrlich gesagt noch nie gesehen...). Der hat dann nix mit C zu tun.

4. Es gibt den ESP und den EBP. Stack Pointer und Base Pointer.
Ich habs inzwischen leider vergessen, aber die brauchst du beide um lokale Variablen anlegen zu können (musst googeln wie das genau funktioniert).
Ich glaub man hat den Stack Pointer gesichert, den BP auf SP-xyz gesetzt und dann per EBP+x auf die lokalen Variablen zugegriffen.

5. Eigentlich sollte der auf dem Stack liegen, die Register werden zur Argumentübergabe normalerweise nicht genutzt, da jede Arch andere hat.
Es reicht aber wenn du der Funktion einen Pointer übergibst wo diese dann Daten verändern kann.

6. kann ich dir ehrlich auch nicht weiterhelfen, ich hab für sowas nicht die NASM Funktionen genommen sondern die Parameterauswertung etc. selbst geschrieben.
 
Danke schon mal für die Antworten. muß die noch ein paar mal durchlesen, um es zu verstehen. ;)

soviel vorweg:

2) Ich meinte ja, daß du in nasm auch Variablen deklarieren kannst. Mußt du für diese dann stack reservieren? Kann man das nciht einfach haben, daß sich nasm darum kümmert?

3) Guck auf mein Zitat. Ab 186 gibts den Befehl.
 
Also den Befehl kenne ich tatsache nicht, hab ihn auch noch nicht gesehen - durchaus möglich, wenn der C Compiler zb. lokale Variablen anders anlegt als Intel das beim 186 ersonnen hat.

Mit Nasm hab ich noch keine Variablen deklariert, da kommt einfach ein DB, DW oder DD irgendwohin und damit hat sich das ;).
 
Nur daß ich das verstanden habe: mit DD etwa definiert man sich DWORD *Konstanten*, richtig?
 
Nein.

Hör auf in einer Hochsprache zu denken, die Zeit ist jetzt vorbei ;D.

Mit DD 0x0a sorgst du dafür, dass an dieser Stelle im Code ein Double Double Word (also ein Quad Word, 8 Byte) mit dem Wert 0xa steht. Also genaugenommen in Hexadezimal 0x000000000000000a (nach Big Endian, 0x0a00000000000000 nach little Endian).

Wenn du jetzt in Nasm sowas schreibst wie "var1 DD 0" wird an dieser Stelle im Code auch nur ein DD mit dem Wert 0 geschrieben. var1 ist ein einfaches Label, und wenn du zb. MOV EAX, [var1] ausführst, wird var1 während des assemblierens in ein einfaches Offset umgewandelt, an dessen Stelle sich zufälligerweise DD 0 befindet ;).


E: Das war jetzt natürlich ausgemachter Unsinn, man wird eben alt.

DD steht für Data Double Word, ist also nur 4 Byte lang. Entsprechend gibts DB (Data Byte), DW (Data Word), DD (Data Double Word) und DQ (Data Quad Word).
 
Zuletzt bearbeitet:
Ok, das mit der Größe habe ich verpeilt. ;)

Was würde denn passieren, wenn ich versuchte an betreffendes Offset zu schreiben? Gehts das gut oder nicht? Wenn es denn ginge, wozu gibt es dann segment .bss bei nasm? Nur für uninitialisierte (aber reservierte) Speicherbereiche? (Ich versuche das Wort Variable zu vermeiden. ;))
 
Nein, das mit der Größe hab ich verpeilt ;). Habs oben editiert.

Sitze hier gerade in einem Netten Dachzimmer bei 34°C... also nicht wundern *chatt*.


An das Offset kannst du schreiben, und die Segmente brauchst du bei NASM eigentlich garnicht zu beachten. Das ist nicht für die eigentliche Laufzeit, sondern für die Ausgabedatei die NASM erzeugt - bei zb. ELF (und so ziemlich allen anderen Formaten auch) wird alles in einzelne Bereiche untergliedert, damit beim Ausführen der Datei auch alles an die richtigen Stellen geladen wird. Bei x86 hast du da weniger ein Problem, weil es zb. kein NX Flag gibt (bzw. das nicht verbreitet ist).

Bei anderen Architekturen wird da streng getrennt, und da müsste das "var1 DD 0" in einen Datenbereich, damit du lesen/schreiben kannst.
Dein Code dagegen muss in einen Codebereich, damit du den ausführen kannst.

Wie gesagt, das betrifft aber nur die Ausgabedatei, nicht das Prog selbst zur Laufzeit. In der .ELF steht dann zb. sowas drinnen:

.code -> an offset x laden
.daten -> an offset y laden
.stack -> an offset z laden (wir haben ja einen linearen Speicher, ohne Segmente oder sowas)

entsprechend werden die Pages dann geladen und konfiguriert (code/daten/daten). Wenn in der ELF steht, dass .daten nach 0x100000 geladen wird und var1 das erste ist was in .daten steht, bekommt var1 eben das Offset 0x100000. Und von/auf diese/r Stelle im Speicher kannst du dann lesen/schreiben.

.code dagegen wird in einem Bereich liegen, den du höchstens lesen kannst, aber auf jeden Fall ausführen kannst. Bei IA32 existiert die Trennung nur der Form halber, IA32 kann solche Bereiche nicht in Hardware unterscheiden, wesswegen du da auch in den .code Bereich schreiben kannst.
 
Aha, Mann bin ich froh, daß du da bist, so langsam lichtet sich das Dunkel. :)

So, dann wäre im wentlichen doch nur eine Sache, die ich immer noch nciht ganz gecheckt habe. Wieder zum Bspcode:

Code:
_myfunc: 
        push    ebp 
        mov     ebp,esp 
        sub     esp,0x40

DIe 0x40 entsprechen ja dem ersten Paramter bei ENTER. Ich frage mal so: Wann muß ich da einen Wert !=0 angeben? Ich habe jetzt ein paar Bsplistings gesehen und da wurde eigentlich immer ENTER 0,0 übergeben.

Muß ich mir den Stack "reservieren", wenn ich in der assembler Routine etwas auf den drauf pushe? (Wäre irgendwie unlogisch aus meiner Sicht.) Oder wenn der "caller", also die C Funkt lokale Var enthält. (noch unlogischer...) Denn mittlerweile sehe ich nciht, wie ich dennin meiner assembler Routine lok. Var hätte. durch .data und .bss kriege ich irgendwelche Speicherbereiche, Ok, nur in jenen Bsplistings wurde trotz diesen ENTER 0,0 benutzt.
 
Es gibt statische und lokale Daten. Ist ein wesentlicher Unterschied.

Statische Daten belegen die gesamte Programmlaufzeit über einen Speicherbereich, und verlieren niemals ihren Wert. Wenn du in C in einer Funktion etwas deklarierst ist das nicht statisch, wenn du dazu static angibst wird es statisch gemacht.

Lokale Daten dagegen liegen im Stack, genauer gesagt in einem Bereich der zu Funktionsaufruf "reserviert" und zum Funktionsende wieder "freigegeben" wird.

Also erstmal wie der Stack funktioniert:

Für den Stack wird irgend ein Anfangspunkt festgelegt, meinentwegen xyz. Der ESP (extended Stack Pointer) speichert dabei die Position, wo gerade das Ende vom Stack liegt - am Anfang, wenn der Stack leer ist gilt also ESP=xyz.

Wird nun was auf den Stack gelegt, wächst der "rückwärts" - ESP wird also immer kleiner.

So ungefähr:

; ESP=xyz
PUSH AL
; ESP=xyz-1
PUSH EAX
; ESP=xyz-5
PUSH BX
; ESP=xyz-7
POP EAX
; ESP=xyz-3
PUSH ESP ; ja, das geht auch ;)
; ESP=xyz-7


usw. usf... btw. der 8086 hat bei PUSH SP übrigens einen kleinen Bug ;).


Wie gesagt, dabei bin ich schon etwas eingerostet. Na mal sehen ob ich das zusammenbekomme...
Den alten EBP dürfen wir nicht überschreiben, daher wird der auf den Stack gelegt. Den neuen EPB setzen wir auf das aktuelle Stackende und ziehen vom ESP selbst 40 Byte ab, damit haben wir 40 byte für lokale Variablen (per ESP+1..40).

Wenn du die Variablen in .data oder so packst werden die statisch und nicht lokal ;). Dann kannst du da eine 0 nehmen
 
Ok, wie der stack funktioniert (im Groben) habe ich schon verstanden. Frage ich mal anders herum: Wann brauch ich das in assembler bzw wie kann ich da lokale Daten haben? Hättest du ein kleines bsp listing?

BTW 0x40 != 40 bytes. ;) Ja ja, die Hitze. ;D
 
:P

Das stimmt natürlich, 0x40=64 Bytes.

Lokale Daten, tja, kannst du einfach haben.

Wenn du eben mal mehr Daten brauchst kannst du statt statischer Daten oder statt Registern eben auch lokale Daten nehmen.
Das musst du schon selbst entscheiden, was du nimmst ;). Wenn du zb. für eine Init-Routine 1MB Speicher brauchst wirst du den sicher lokal wählen wollen, weil der sonst dauerhaft vergeben ist. So wird der Speicher nur beim Funktionsaufruf benötigt.

Von der Sache her fragst du gerade, ob du nun "int a" oder "static int a" nehmen sollst ;). Praktisch gibts nur in seltenen Fällen einen Unterschied, in Assembler sind statische Daten besser lesbar wohingegen in C lokale Variablen sauberer sind.

Also du könntest eben sowas machen:

Code:
push    ebp 
mov     ebp,esp 
sub     esp,0x40  

; tu was kompliziertes

; Speichere Ergebnis
MOV     [ESP+4], EBX

; tu nochwas kompliziertes was alle Register braucht

; rechne mit altem Ergebnis weiter
MOV     EBX, [ESP+4]

Wenn du die Funktion verlässt sind die Daten auf jeden Fall ungültig.
 
Naja, so ganz habe ich das immer noch nicht gecheckt. dein mov [esp+4], ebx ist doch äquivalent zu push ebx, oder? Was würde denn passieren, wenn ich das sub nciht vor dem push durchführen würde? Bei etl. Routinen sehe ich ja ein pusha, obwohl enter 0,0 aufgerufen wird. Naja, ich werde mal weite nach code umsehen, wo enter x,0 benutzt wird. Evtl geht mir dann ein Licht auf. ;) Ich denke im Moment komme ich auch so ganz gut aus.

Ne andere Frage: Solange ich auf den stack nicht zugreife, kann die Werten von ebp und esp einfach etwa in einem .data Bereich auslagern und dann mit zwei weiteren Registern hantieren (und dann wieder alles zurückhole)?


Noch ne andere Frage: Unter welcher IDE entwickelst du assembler? Ich kriege das wie gesagt mit kdevelop nciht so ganz hin. :(
 
Ach ich ...

Hätte natürlich [EPB+...] heißen müssen :P

Das ist ja gerade der Vorteil, denn so kann man trotzdem Daten auf dem Stack ablegen - EBP verändert sich nicht, wohingegen sich ESP ja verändert.
Und der Stack fängt auch nach den lokalen Variablen an, kommt mit denen also auch net ins Gehege.
 
Also ich blick im Moment auch nicht mehr so durch, ist wie gesagt schon etwas her.
 
Naja nicht weiter schlimm. Hast mir ja schon gut weitergeholfen und werde mal sehen wie weit ich komme. :)
 
OK, meine ersten Gehversuche. Habe mich mal an OpenAL versucht und eine Funktion in assembler umgewandelt und es geht auch. Kannst ja mal gucken und sagen, was ich besser machen könnte. Ich habe zwei Varianten für die Schleife. Ich denke die zweoite wird besser sein.

original
Code:
void _alChannelify2Offset( ALshort *dst, ALuint offset,
			   ALshort **srcs, ALuint size ) {
	ALshort *src0 = &srcs[0][offset / sizeof *srcs];
	ALshort *src1 = &srcs[1][offset / sizeof *srcs];
	ALuint k;

	size /= sizeof *dst; 

	for( k = 0; k < size; k++ ) {
		dst[0] = src0[k];
		dst[1] = src1[k];

		dst += 2;
	}

	return;
}

meine Assembler Funktion:
Code:
%include "c32.mac"

proc _alChannelify2Offset

%$dst		arg(4) 
%$offset	arg(4) 
%$srcs		arg(4)
%$size		arg(4)

push esi
push edi
push ebx

mov eax, [ebp + %$offset]

mov ebx, [ebp + %$srcs]
mov ecx, [ebx]
add ecx,eax	;source 0 start
add ebx, 4
mov edx, [ebx]
add ecx,eax	;source 1 start

mov ebx,  [ebp + %$size]
shr ebx, 1

xor esi, esi

mov edi, [ebp + %$dst]

.loop
;original loop
;mov ax, [ecx+esi*2]
;mov [edi], ax

;mov ax, [edx+esi*2]
;mov [edi+2], ax

;modified version
mov ax, [edx+esi*2] ;source1 will be in high word
shl eax, 16

mov ax, [ecx+esi*2]
mov [edi], eax

add edi, 4
inc esi 

cmp esi,ebx
jne .loop
;loop_end

pop ebx
pop edi
pop esi

endproc
 
Zuletzt bearbeitet:
Hab nur mal kurz drübergelesen, heute Abend schau ich mir das vielleicht noch genauer an. Versuch so oft wie möglich 32bit Register und 32bit Speicherzugriffe zu nehmen, 16bit Zugriffe sind inzwischen langsamer als 32bit Zugriffe.


Hmmm... die Funktion kopiert ja src nur nach dst, und das wars.

Einfachste Variante wäre die Stringbefehle zu benutzen, das wird zwar inzwischen nicht mehr so gerne gesehen weil die auf Byte-Basis arbeiten aber da brauchst du nur einen einzelnen Befehl.

Ich weis dummerweise nicht mehr welcher das war *buck*, aber das sollte dann so ungefähr aussehen:

Code:
LEA    EDI, [src]
LEA    ESI, [dst]
MOV    ECX, [k]
STOSB  EDI, ESI

STOSB (glaub das war er, könnte sein, dass es da inzwischen auch 32bit Entsprechungen gibt, glaub mal was von STOSW gelesen zu haben) läuft solange bis ECX gleich 0 ist.

Eine alternative wäre auch REP. Hab aber gerade meine Assemblerlektüre nicht zur Hand, kann also nicht nachgucken wie der Syntax ist (Assembler vergess ich irgendwie immer :]).
 
Nee, ist leider keine einfach Kopierfunktion. Dachte ich beim ersten mal auch. Die Funktion erzeugt aus 2 streams einen interleaved stream.

Das mit den 32bit Zugriffen hatte ich schon gedacht, darum denke ich, daß meine 2. Variante besser sein wird.
 
Achso. Wie groß sind denn die einzelnen Elemente? Da guck wir mal ROR/ROL an, damit bekommst du das sicherlich schneller und besser hin.
 
Hmm weis ich jetzt ehrlich gesagt auch nicht... es war wieder warm heute ;)

Mir ist aber noch eine gute Lösung eingefallen, komplett mit 32bit Zugriffen.

Code:
MOV  EAX, [src0]
MOV  EBX, [src1]
MOV  ECX, EAX
MOV  EDX, EBX

SHR  EAX, 16
SHL  ECX, 16
SHR  EBX, 16
SHL  EDX, 16

OR   EAX, EDX
OR   EBX, ECX

MOV  [dst], EAX
MOV  [dst+4], EBX

Da hast du in einem Rutsch gleich 8 Byte interleaved, mit 16bit Blöcken. Falls irgendwelche Fehler drinnen sein sollten ist das kein Wunder, mir brummt der Schädel ein bisschen.
 
Ich habe deines noch nciht ganz durchdacht, aber ich hatte schon an was ähnliches gedacht. Den Brummschädel habe ich heute auch. ;) Nur müßte ich bei meinen Überlegungen ein zusätzliches Register bemühen und auch zwichen gerade und ungerader size unterscheiden. Deines ist aber ganz 32bit, was gut sein sollte. Mal gucken...

OK, deines müßte klappen, allerdings sind die Register bei dir anders organisiert und müßte verstärkt auf den Speicher zurückgreifen. Bei deiner Lsg müßte ich etwa die offsets von src und dst in den Speicher schreiben, während es bei mir in den Registern bleibt. Man müßte mal messen, was schneller ist.
 
Na wie du die Adressen in die Register bekommst musst du dir noch überlegen, war nur ein Beispiel.

2 könntest du in ESI und EDI speichern, die 3. Adresse vielleicht in einem FPU Register (oder du schaltest vorher auf MMX um, dann ab damit ins MMX Register).
 
Zurück
Oben Unten