Re: allocating memory.
From: Beth (BethStone21_at_hotmail.NOSPICEDHAM.com)
Date: 12/18/03
- Next message: Robert Redelmeier: "Re: allocating memory."
- Previous message: Toby Thain: "Re: OT: Whiny CS Majors"
- In reply to: Jumbo: "allocating memory."
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Date: Thu, 18 Dec 2003 15:17:34 -0000
Jumbo wrote:
> Hello
Hi :)
> I am moving from C++ into assembly and I was trying to compare
memory
> allocation. So what would be the ASM equivalent of new memory
allocation, or
> the free-store, in C++?
> In segment:offset and flat modes please. :o)
> TIA.
Memory allocation is primarily an OS responsibility, as many programs
require memory resources (and may even be "competing" for it :) and a
"free for all" would likely result in a disaster (with ten programs
all deciding to grab overlapping parts of memory regardless of other
program requirements in the system, writing over each other's data and
code...a perfect recipe for a monumental unrecoverable crash of the
highest order ;)...
Hence, OSes are almost certainly responsible for memory
allocation...basically, so as to act as "referee" or "judge", if you
will, as to ensuring that memory is allocated without any "conflicts"
(as all memory must then be allocated and deallocated _via_ the OS,
whose code is written to avoid causing "clashes"...usually, for
typical non-shared memory, by simply allocating a specific range of
memory _only_ for a single program that no other program will be
allocated :)...in fact, though modern OSes - especially Windows - have
greatly increased OS responsibilities, this is the essential,
fundamental reason _why_ OSes exist at all...they've grown from simple
memory allocators to more or less controlling _everything_ in a system
to provide "portability" (your program communicates with the OS in a
"device-independent" way to provide commands...and then the OS - via
device drivers - communicates to the actual hardware these commands in
the "device-specific" way that the hardware understands...thereby,
your program need not be able to communicate with thousands of
hardware devices in a device-specific way - which would make even the
simplest programs impractically complex to write, as the range of
hardware that is programmed in completely different ways would all
have to be dealt with individually and there's simply too much
hardware that could be plugged into a PC system - it merely needs to
communicate with the OS...and each hardware device has a "device
driver" which makes the conversion between the OS's
"device-independent" communications to things that the hardware
understands...thus, every program - the OS included - can use the same
"device driver" code, which need only be written once, usually by the
hardware manufacturer themselves and distributed with that
hardware...and this is basically how hardware "portability" is
achieved with modern OSes :)...and to also cover the greater range of
"resources" that could be potentially "shared" (memory is the most
obvious but, especially with multi-tasking OSes running multiple
processes, practically everything in a system is "shareable":
printers, video resources, sound resources, hard drive, etc.
:)...older OSes like DOS, though, only has facilities for sharing
memory and disk resources between multiple programs (and the fact it
bothered with the disk was, at the time, sufficiently impressive that
this is what the name makes a big song and dance about being DOS's
great "capability"...yup, it's _the_ "Disk Operating System"!! Be
impressed...be very impressed...NOT! ;)...
High-level languages like C++, though, hide the details of the actual
OS specifics for memory allocation...specifically behind things like
"new", "delete", "malloc", "free" and so forth...these languages do it
like this so that they aren't dependent on any one particular
OS...using "new" in C++ gets translated by the compiler during
compilation into the OS specific method for memory allocation
appropriate to the OS that the program is being compiled for...in this
way, you can take the same program over to a different compiler and
different OS without needing to worry about the fact that one OS
actually does memory allocation in a different way to other OSes
(generally, they all have their own _unique_ methods and there's no
direct "compatibility" at all...the exception here is UNIX-based OSes
because those OSes actually make the effort to try to be "compatible"
with each other...and, as C was developed to write UNIX, then having
"malloc" or something similar as part of the OS itself isn't unheard
of :)...
As you know, I'm sure, assembly language has no such "hiding" policies
because it's, basically, the actual machine code that literally runs
through the CPU, just re-written into "mnemonics" and other
human-readable text to make it easy to deal with (rather than some
long string of binary or hex which you have to learn to "decode" in
your mind...there are some people who can do this - a bit like how
those people in the Matrix films have learnt to be able to "read" the
streams of nonsense falling down the screen to be able to "see" what's
going on in the Matrix - but as it would take a _very long_ time waist
deep in hexadecimal code to even begin to master the art (and, well,
there's not actually that much of any advantage in doing so,
anyway...though, "Binary" Brad and Wolfgang may disagree on that last
point as they _Love_ hex code ;)...then most people stick with the
"mnemonics" of assembly language, which at least begin to approach
looking like recognisable words...
Anyway, with such low-level programming, the high-level languages
"abstractions" simply aren't present...which is the fundamental
"advantage" _AND_ "disadvantage" of assembly programming: _EVERYTHING_
is "exposed"...that's good in that you can access everything that it's
permissible to access and program it on "its own terms" to take full
advantage of it...within this "sphere of Liberty" is where the
potential of assembly language to out-compete any other language
exists...similarly, this can also potentially be the very thing that
freaks out learners of assembly language in that, indeed, _everything_
is "exposed"...that, for a particular task, you might have to learn
about the CPU, the OS, the hardware, some OS libary, etc. all at the
same time to achieve it...and, along the same lines, as everything is
exposed and we're working on the lowest level, then practically _ALL
RESPONSIBILITY_ falls on the assembly language programmer's
shoulder...that's another bit of potential "culture shock" HLL
programmers can experience in coming to ASM (unfortunately, many who
get such a "culture shock" from the quite radically different nature
of assembly automatically "retract in horror" at what they find...and
then run around telling everyone else: "assembly is impossible!",
"assembly should be left to compilers, as all I got was headaches
trying to do it myself!" and a lot of "assembly myths" which do the
rounds...an understandable reaction, I suppose, but too much of this
has given ASM an unfair "bad reputation"...okay, the odd thing _is_
slightly deserved - it is, for instance, "a bit weird" but that's
because we're speaking "Computer-ese" itself (what the CPU "speaks" ;)
and computers are weird things...but, well, it's "part and
parcel"...like you can't go shopping without spending money, as much
as we'd all Love to have things without having to hand over any money
at all...you want one, you got to put up with the other...just the way
things work - but an awful lot _isn't_ deserved...convincing people
that if they just stick with it for a few weeks that it _will_ all
start to fit together in their head - and, even, dare I say, become
almost "too simple" - is often next to impossible because of all this
stuff ;)...
So, the fact that every OS provides its memory facilities in usually a
completely different way to all the others, is a fact you do now have
to deal with, which HLLs usually spared you from needing to know
anything about...hence, now we can begin to understand _why_
everyone's been saying "memory allocation isn't part of assembly, it's
part of the OS and DOS does it like this, Windows like that, Linux
like this, etc."...note, mind you, that it's really the same in C++ -
"new" and "delete" are library functions that go on to call something
like "VirtualAlloc" and "VirtualFree" under Windows, as a particular
example - but you're "spared" from knowing this...the details get
hidden inside the "new" and "delete" functions...
Which is an interesting thing because we can actually compare C++
memory allocation to ASM memory allocation...because, of course, in
the end, C++ boils down to assembly / machine code so ASM is always in
the "anything you can do" category...
Static allocation is the simplest; In C++, this means defining a
(static) global variable (also, if you put "static" in front of a
local variable in a function, it's also static allocation...the
function only effects the "scope" of the variable so that it can only
be "seen" within that function...but, in terms of storage, a local
"static" variable in a function is stored in the same way as a global
variable...which is why "static" local variables - like their global
variable counterparts - _retain_ whatever value they had the last time
we were in the function when the function gets called again..."static"
local variables, really, _are_ global variables in nature but the
"scope" - what the compiler permits to be "seen" - is reduced...it's a
"global variables" that "hides" itself so it can't be "seen" when not
in the function that defined it :)...basically, the final output
executable file is just a long string of "binary", so to
speak...clearly, lots of this is the binary "opcodes" that correspond
to the source code that was compiled / assembled (e.g. it's the
machine code of your program :)...but you can also simply have a chunk
of this binary data that's just space "reserved" for data
variables...because, in the end, machine instructions themselves -
your code - are also just "binary data" too (it's just "data" that the
CPU interprets according to its "rules" to be a series of instructions
:)...on-disk and in memory, it's, in the end, all just a long string
of binary data and "static" allocation is just a case of "reserving"
some of that for data variables rather than your code...
Thus, in ASM, this type of "static" allocation is very easy
indeed...if you simply put a data declaration in between procedures or
at the end of the file after all the code (don't put inside an actual
procedure, though, as the CPU simply executes the instructions
one-by-one and if it hits the data then it tries to interpret whatever
that data is as another machine instruction...as I noted above,
machine instructions themselves are actually "data" too - everything
inside the machine eventually boils down to just being "binary data" -
what turns it into "code", basically, is that the CPU fetches in the
data and then tries to interpret the values as "opcodes" - machine
instructions - and performs what it finds...hence, if you stick data
in the middle of an instruction "stream", the CPU will blindly carry
on thinking it's just more instructions...this could lead to a nasty
crash where it interprets your loop variable to be a "jump to
somewhere random in memory" instruction instead...so, keep your data
out of any "instruction stream"...but, otherwise, you can stick it
where you like, in a place where the CPU executing instructions won't
ever actually reach...between procedures, after the end of your code
or you can code a "jmp" instruction before the data that "jumps over"
it and that'll work too...assembly has no strict "layout" rules other
than it should, obviously, make sense for the CPU when it tries to
execute it...note, though, that this "code is data too" thing has
advantages...if your assembler doesn't support a particular
instruction - say, it's a little older than the latest generation of
chips - then you can look up its machine code equivalent and can put
that data directly into the code...making yourself - at least for one
or two instruction - a "hardcore hexadecimal machine code
programmer"...an assembler converts the "mnemonic" into its machine
code equivalent...but if it lacks a particular "new" instruction then,
well, you can always do the "conversion" manually and insert the
machine code yourself as "data" in the middle of the code...although,
of course, in doing so, you've got to be a little careful you don't
mess it up...another variation on the "code is data too" theme is
"self-modifying code"...one section of code can "manipulate" another
section of code as though it was just some "data" and a program can
"re-write" itself as its running (!!!)...and, lastly, you can also
simply create your own little "compiler" that compiles a bunch of
machine code instructions into an area of data and then simply make a
"CALL" or "JMP" to that data, which allows things to be "compiled"
on-the-fly (for example, something like a "Just-in-time (JIT)
compiler" could be using this technique to compile some HLL into
machine code as its running for that half-compiled / half-interpreted
flavour ;)...
There's two main flavours of this "static" data...the "initialised"
variety (called just "data" usually :), where you actually slot in
values in the "reserved" sections of memory...then, when the OS loads
in the executable file from the disk, these variables are
"initialised" to these values because those values are literally
written into the on-disk file itself...this contrasts to
"uninitialised data" (called "BSS"...the name is historical and, you
know what, I'm not sure what it stands for myself...one of the "Ses"
has got to be "storage", I reckon...anyway, what it stands for isn't
particularly important...what it _is_, though, is the important
bit...and it's all your "uninitialised data" :)...this data has no
particular values when the program starts (e.g. you've put "static int
VarA;" rather than "static int VarA = 3;" :)...because it doesn't have
any values initialised then actually storing this data in the on-disk
file would be a waste of disk space (why store some random "don't
care" value in the file? :)...therefore - although not all file
formats necessarily have all the "header information" needed for this,
so some don't support the idea of "BSS" data - what can be done
instead is to add up how much "uninitialised data" there is and store
that in the header...then, the actual executable only needs store code
and _initialised data_ - the parts that have specific values when the
program starts - in the on-disk file...the "BSS" is dealt with (on
OSes that support the concept) by just allocating some extra memory
after the executable file in memory...because uninitialised data has
no initial value then we "don't care" what value it starts at (usually
because the program itself will set that whilst running :)...hence,
any old "extra memory" allocation will do (whatever it's automatically
set to when allocated...although, often, allocated memory is
automatically zeroed out, which is why uninitialised variables in C++
will also tend to be zero, if you "naughtily" try to use them before
initialising it...but, strictly, the actual value that allocated
memory might be is "undefined"...often, it's just zeroed out but it
might not be...there's no guarantees so it's best not to assume
anything about it and initialise it in the program itself :)...this
"BSS" stuff is really just a "space saver" in that, due to what they
are, uninitialised data doesn't have to be actually put into the
on-disk file...rather, the OS will load that on-disk file into memory
and just also allocate the "extra" needed to cover the BSS directly
after what it just loaded, "as if" it had always been there at the end
of the file and was just loaded in...being "uninitialised data", of
course, we can do this because these data variables don't need to be
set to anything specific as they are all "don't care what value they
are when the program loads" variables...BSS doesn't have to be used
and not all OSes or all executable file formats have the necessary
support to use it...but, when it's available, it's a "neat" little way
of making the final executable file on-disk slightly smaller because
the nature of "uninitialised static data" means that this little trick
can be used to save some disk space...hence, you could write a program
that has some really big static array but which isn't initialised and,
using the "BSS" trick, none of this array needs to be literally stored
in the on-disk file...the OS loader will simply allocate some space
for it directly after the file it loads in to make room for it in
memory...
Now we move onto the "dynamic" memory stuff; The most obvious from C++
programming is local variables inside a function...if you've ever
wondered why these variables don't retain their values between calls,
then the reason is about to become apparent...such variables are
actually allocated on the "stack"...the "stack" is just a big
"scratchpad" area of memory for "temporary" things...the CPU has
instructions - PUSH and POP - for putting things on and pulling things
off the stack...the SP (16-bit) or ESP (32-bit) register contains the
memory address of the "top of stack"...now, as the name is meant to
suggest, the stack works like...well...a stack...that is, you're
cleaning your plates after a meal...you clean the first plate and put
it ("push" it :) to one side...you clean the next plate and then
"stack" it on top of the first plate...the next plate goes on top of
that...and so on and so forth...now, if you want to take a plate off
the stack, then you take it back off the top of the stack (hence why
the example is about plates...to remember that you always put things
on and take things off the _top of the stack_ always, you can imagine
this tall stack of plates and imagine what would likely happen if you
tried grabbing, not the top, but the bottom plate in the stack and
tried pulling it away...oops! There comes falling all the plates onto
your head! Smash! Crash! "Ooh, that was me best china an' all!"
;)...in technical terms, this is called a FILO ("first in (is) last
out") data structure...because, with the stack, we only add and remove
things to the "top of stack" all the time, that's why we only need one
register - (E)SP - to keep track of the "top of stack" address...
If you want to temporarily store away the AX register (because you
need to temporarily use it for something else :), then you can "PUSH
AX" and the value of AX gets put onto the top of the stack...do
whatever you want to do with AX...then, you can get back the value you
temporarily stored by using "POP AX" (which grabs the top of stack and
then puts it into the AX register :)...the stack, though, is also
commonly used to: store the "return address" when making a "CALL"
(that is, the CPU "pushes" the current address onto the stack, then
jumps to the procedure that the CALL specifies...when that procedure
ends with a "RET" (return) instruction, the CPU "pops" the stored
"return address" off the stack and then jumps back to this address, so
that it can carry on executing your program from where it originally
was, the instruction straight after the "CALL" instruction...thus,
procedures can be CALLed from anywhere and even be called recursively
(the procedure directly - or indirectly via other procedures - calling
itself in a circle :) and this is just, well, pushing more and more
plates onto the stack...and as long as there's a "RET" at the end of
each procedure, it'll "pop" those "plates" (return addresses) back off
the stack in exactly the _reverse_ order it called them in...by using
a stack for this, the CPU can CALL procedures all over the place and
doesn't get "lost"...it always records where it was last called from
on the stack so that it can "backtrack" as it "RETurns" from all the
procedures, right back to the start :)...another common use of the
stack is to pass parameters (HLLs do this almost religiously as part
of their standards for their "calling convention"...that is, the rules
that particular language uses for using the stack for passing
parameters...simple example: C pushes parameters right-to-left - that
is, the parameters that furtherest on the right in the C source code
gets pushed first and it "works backwards" to the first parameter -
and it's the responsibility of the procedure that called - the
"caller" - to clear up these parameters from the stack...Pascal
"convention" is left-to-right and the called procedure is the one
responsible for cleaning up the stack to clear the parameters...these
"conventions" are, basically, just the default ways these HLLs use the
stack to pass parameters...there are others and you can declare
something like "_pascal" convention with many C++ compilers to make it
use the Pascal convention, overriding the usual default C parameter
passing conventions...note, there's no such thing as an "ASM calling
convention" because one of the advantages of using ASM is that you can
pass those parameters in any way you see fit...in a register, on the
stack (you choose what order), via some global variable or some mix of
any or all of these...you have totally Liberty to do whatever you
like...the HLL compilers, though, on the other hand, just churn out
code automatically and, therefore, they follow these "conventions"
just to make sure that all the code fits together...calling what's
actually a C "convention" function - it's expecting parameters
right-to-left and that the "caller" will clean things up - following
the Pascal "conventions" to make the call...well, oh dear, things will
get nasty...the parameters will all be "backwards" and either there's
too much "clean up" of the stack (both "caller" and "callee" do
thinking its their responsibility when actually only one should be
doing it ;) or not enough "clean up" (neither cleans up because it
thinks the other one should be doing it :)...and if the stack gets
"out of synch" you could be in some serious trouble because, remember,
all those "return addresses" from procedures are automatically pushed
and popped from the stack by the CPU when you use "CALL" and
"RET"...thus, if the stack gets all confused then it could "pop" what
it thinks is a "return address", which actually isn't and it'll
suddenly jump somewhere slightly random in memory and try to execute
that as instructions...very nasty and, unfortunately, often slightly
to easy to do in letting the stack get all confused...as mentioned,
the advantage and disadvantage of assembly language; Advantage:
everything is "exposed" so you get to do whatever you like and can do
some really neat tricks that can't be done in many
HLLs...disadvantage: everything is exposed so you've got to be careful
not to stick your hand in the wrong place or you could get fried for
doing so...that is, ASM provides _complete Liberty_ (if it's
permitted - OSes can limit things by using the CPU's "protection"
devices (usually - or should that be "supposedly" - in the interests
of stopping a program doing something it shouldn't...such as
overwriting other programs or trying to take over the system...be that
accidentally (buggy code) or maliciously (it's a virus!)...basically,
the "protection" stuff added onto the CPU - which the OS grabs hold of
when it boots up and doesn't let go so that it gets "control" over
these "protections" and can enforce some "security" and "safety"
(though, in Microsoft's case, you often wonder if they actually
understood those chapters of the Intel manuals, as "safety" and
"security" aren't the first words that come to mind when thinking of
Microsoft OSes ;) - _does_ limit your Liberty in some ways, according
to the OS's "policy" on this...but it's (at least, supposedly) "good"
limitation because it catches buggy code and stops viruses and things
like that...or, at least, this is the "theory"...the practice is that
it helps out but the virus writers always find some "weak spot" in
Windows - often because MS seems to have a bizarre "policy" of adding
"weak spots" in their OSes for this purpose (or, at least, they are
often so bad at this, you almost think they _want_ to have viruses and
buggy applications ;) - and gets around it :)...
Anyway, yes, ASM provides _Liberty_...but the "cost" of this is that
suddenly _everything_ gets dropped on your shoulders...well, that's
putting it melodramatically as it's not that bad...but the point being
that _you_ have to be careful about keeping that stack in order...if
you're still learning this stuff, then, basically, you _will_ mess it
up once or twice, at least...but this is a good learning experience
which teaches you how to be extra careful from then on...the essential
thing to remember about the stack is to keep it "balanced"..."pop" off
what you "push" on (remembering that things come _off_ a stack in
reverse order to how they were put _on_ ;)...make sure that the stack
at the end of a procedure (just before the "RET") is where it was when
you first entered the procedure after the "CALL" (because then you're
"popping" off the correct "return address" to get back "home"
:)...that sort of thing...there _are_ places where you delibrately
leave a stack "unbalanced" (because some other bit of code will
eventually sort it out later :) but these are rare-ish "special
cases"...unless you have good reason, always think "balance" - in a
kind of Ying-Yang Eastern way - when using the stack...
And the stack is also used for - eventually getting back to the main
thread - local variables...you just "reserve" some of the stack space
for your variables and do all your calculations and things using this
reserved space...then, when you've finished at the end of the
procedure, you "pop" it all off...so, back to the question: why do
local variables in C++ programs lose their values in between function
calls? Yup, because they are allocated space on the stack...which is a
structure for _temporary_ data...when not inside that function, its
local variables, in fact, don't even actually exist...they get some
"temporary" space allocated on the stack during the function call and
that gets given back at the end of the procedure so they "disappear"
again...remember, when you made the "CALL" to the procedure, the
important "return address" was pushed onto the stack...therefore, you
more or less (that is, you _must_ unless you're delibrately playing
some "dirty hacker tricks" with your stack ;) _must_ give back the
temporary space for the local variable because you need to get back to
that "return address" further down in the stack...
[ Oh, speaking of which, there is _one_ particularly confusing thing
about stacks that's worth mentioning...physically, in memory, stacks
are actually "upside-down"...that is, if we presume the kitchen
ceiling is "top of memory" (higher addresses) and the ground "bottom
of memory" (lower addresses), then we're sort of defying gravity and
"pushing" these plates onto the ceiling and the stack comes
"downwards" to the floor...well, gravity doesn't exist on data inside
memory chips...note, it works exactly as you'd imagine it would were
it the other way around, like I was saying about not pulling the
bottom plate because it would all come crashing down...it's just that
physically in memory the stack is "upside-down"...pushing _decreases_
the memory address, popping _increases_ it...hence, when I said "down"
above, I actually meant "up"...yes, sorry about this...it's a touch
confusing...but don't blame me, I didn't design it...it's probably
best to imagine a stack the "right way up" normally but just
remembering that in memory address terms, it's actually the other way
around...where our plates stack "up" towards the ceiling, this would
actually be going "down" in terms of the memory address...there is a
reason for this organisation but merely mentioning this is confusing
enough without all those details as well :) ]
And, to complete this tour of memory, there's nice simple "dynamic
heap allocation"...that is, you call the OS and say "please give me
5KB of memory"...to which the OS either says "sure, here it is" or
"oh, sorry...all the memory's being used at the moment"...you'll be
happy to know that this stuff is nice and easy, after trying to work
out what on Earth all that "stack" business was about...there's an OS
function for allocating memory and another one for deallocating
memory...and, in fact, when you use "new / delete" or "malloc / free",
all these functions do is go on to call the OS functions for
allocation and deallocation...the sole thing to be careful of is the
"memory leak", which you also have to be careful of in C++ that you
probably know all about this...again, a "balanced" view of things
works well with the mantra: "whatever I allocate, I _must_ deallocate"
;)...
The above has been a "generic" way to describe things, leaving out the
OS details to just explain the main memory things available...but, for
comparison, you'll probably like to _see_ what on Earth I'm talking
about...the truth is, C++ is very "bare bones" that the two are
somewhat comparable...the difference is all in the "low-level" way of
looking at things...
Note, the following is done from memory and not actually tested...it's
"equivalent" code (your compiler might not spit exactly this ASM out
for the C++ but it should do the same thing "in spirit" :)...
C++:
-------------- 8< ----------------
static int VarA;
static int VarB = 3;
void Function(void)
{
static int VarC = 8;
int VarD;
void *VarE;
VarE = new char[512];
if (VarE!=NULL)
{
VarD = VarE;
delete VarE;
}
}
-------------- >8 ----------------
MASM / Windows:
-------------- 8< ----------------
.data?
VarA dd ?
.data
VarB dd 3
VarC dd 8
.code
Function: push ebp
mov ebp, esp
sub esp, 8
push PAGE_READWRITE
push MEM_COMMIT
push 512
push NULL
call _VirtualAlloc @16
mov [ ebp + 8 ], eax
cmp dword ptr [ ebp + 8 ], 00000000h
je EndOfFunction
mov eax, [ ebp + 8 ]
mov [ ebp + 4 ], eax
push MEM_RELEASE
push NULL
push [ ebp + 8 ]
call _VirtualFree @12
EndOfFunction: mov esp, ebp
pop ebp
ret
-------------- >8 ----------------
Differences: The most obvious one would be that I can just say "C++"
but, with ASM, I have to specify the assembler and OS...there's a lot
of variety in assembler syntaxes (not standardised)...the Linux or DOS
code would look completely different because they have different API
functions to call (DOS also has a different sized "int" - 16-bit - and
operates in "real mode", which uses a completely different (warped)
style of addressing :)...
Note, on this point, people might say "assembly is
non-portable"...which is true but what is meant by "portable" has to
be qualified a bit better than this...assembly language _is_ specific
to the CPU because, basically, assembly _is_ the language that the CPU
itselfs "talks" (machine code), just translated into something more
"human" - words, labels, numbers, etc. - so that it's not a total
nightmare to read (which a big long string of binary would be ;)...as
_CPUs themselves_ are all different then assembly languages for these
CPUs are correspondingly all different...that's a subtle point that
can sometimes be missed...that _IF_ all CPUs used the same
instructions and encodings and so forth then, in fact, all assembly
languages would be "portable" even here...sounds a bit odd but the
point makes itself clear when you consider that the code above
(should) run on a 80386, 80486, Pentium, Pentium II, III, IV,
etc....though the "core" is the same - which is what makes these chips
"compatible" - there's quite a substantial difference between
them...internally, they are _very_ different indeed...what makes them
"compatible" is that they all retain the instructions and encodings
for those instructions of all the previous chips...one could imagine -
though it wouldn't happen in reality and, really, would we _actually
want_ it to happen when it would very likely hold back innovations,
competition, companies patenting their own technologies (which though
we might not care, _they_ do and if there was no room for these
things, they might all consider going into washing machine manufacture
instead or something ;), etc.? - a "standard" for CPUs that they all
met, allowing assembly language to be completely "portable"...in a
sense, when AMD manufacture x86 chips - which are Intel's design -
that's a small example of this idea actually at work...BUT the usual
problems of "standards" would tend to make this not completely an
attractive prospect across the whole industry..."standards" must be
agreed, changes need to be "negiotiated", the resulting "standard"
would be a "compromise" and would have to favour the "lowest common
denominator"...for a language like C++, this is a "tolerable"
process...but, in this case, "standardising" means literally changing
the technologies themselves and for Intel to think up "MMX" or
whatever, they'd first have to get "permission" and work it into the
"standard"...and all chips would end up being identical that there's
no real difference between them (so there's little incentive for
companies to want to tread down this path when it means effectively
giving up any "technological advantage" that they have, in order to
"let in" other companies)...it's one of those trade-offs: do we slow
down innovation (and, likely, the technology itself at the same time)
but have instantly "portable" code that'll work on any machine...or do
we leave the manufacturers have their "freedom" but this means that
code has to be re-worked a little from machine to machine? Although,
ultimately, the decision is not in our hands but in the hands of the
CPU manufacturers and we know their answer: "let us have the freedom
to compete and innovate, accepting that the price for that freedom is
a little bit of trouble moving code around"...and at about this point,
they'll probably recommend using C++ because it's "portable" ;)...
But though ASM isn't naturally or natively "portable", if we actually
look at how C++ manages this "portability", we can see it isn't too
complex that it can't be copied...note, in the C++ code above, we
actually call "new" and "delete" for the memory allocation and
deallocation...in the ASM code, we call the OS's memory API functions
directly: "VirtualAlloc", "VirtualFree" ("Virtual", as you may have
guessed, refers to "virtual memory" but it's a slightly deceptive name
as you _can_ use the API - with "VirtualLock" and "VirtualUnlock"
too - to specifically get yourself literal _physical_ memory...just
that's a "special case" you specifically ask for so it's "virtual"
because that's what it'll deal with unless told differently...another
simple reason for the name, as well, is that Microsoft used to have
"GlobalAlloc" and "LocalAlloc" and a much more complicated memory
scheme in Windows originally before switching to a nice simple 4GB
"flat" memory model, so the "Virtual" is also just a means to tell the
difference, because for "backwards compatibility" reasons, the other
types of memory allocation are still present...although, because it's
all "flat" and "virtual" now, there's actually no difference between
"global" and "local" memory as there used to be...they just keep the
API in because older source code might still use the older ways and
this'll mean it'll still work :)...
So, in fact, the two bits of example code _aren't_ completely
equivalent...what the C++ compiler actually outputs is a call to a
function called "new" and a function called "delete", which then go on
to call the native OS memory API ("VirtualAlloc" and "VirtualFree" for
this Windows example)...that is, the "OS specific" things are
separated out into "library code" (with a different library for each
OS, containing the appropriate "OS specific" code inside it :) and
then the correct library is "linked" with your code...and, actually,
it's this method by which a large chunk of C++'s "portability" comes
from...if you try writing code _without_ the "standard library" in C
or C++ - using the Windows header files directly - it actually quickly
becomes equally "non-portable"...
Randy Hyde demonstrates this possibility with his HLA ("High Level
Assembly") package...which, like C / C++, comes with a "standard
library" of common routines that can be used straight away and a "HLL
inspired" syntax, just to make things more "familiar" to those who've
used HLLs before (often being accused of "looking like Pascal" ;)...if
you're learning assembly language then you might to look up HLA on
Randy's "Webster" site...it's NOT everyone's cup of tea (some get very
annoyed and angry about this "invasion" of HLL ideas into assembly
language because, apparently, it's not "real assembly"...I won't start
up that argument again, except to note that HLA allows you to program
directly in machine instructions and all the "HLL helpers" are
completely optional...I'll leave it to the "interested reader" to work
out how this doesn't qualify and make up their own minds :)...but it
is designed for exactly the audience of people newly learning assembly
language who've got HLL experience...the idea being to slowly work
your way "down" to proper low-level programming as you learn (the HLL
things helping as "crutches" to use until you learn how to do it in
assembly...a "step by step" kind of plan :), rather than just the
usual "sink or swim" approach...
Anyway, the HLA package demonstrates how certain types of
"portability" are NOT unachievable in assembly language...you merely
have to code in a certain way and take on the responsibility yourself
to _make_ your code "portable"...the HLA library is similar in idea to
C / C++'s "standard library" and has been coded up for both Windows
and Linux...the "portability" is achieved by both the Windows and
Linux versions of the libraries offering the _same_ functions for your
program (though their internal code is, of course, "OS specific" to
Windows or Linux :)...then, your code can simply use the library
functions and you link in the appropriate version of the library for
your target OS...this idea of separating out the "OS specific" parts
and "deferring" it to an external library (where the library is simply
re-written for each OS) is exactly how C / C++ actually achieves
"portability" with its "standard library" too...your program calls
"new" and, inside the Windows version of the "new" function, it goes
onto to call, say, "VirtualAlloc"...inside the Linux version, it goes
onto call "malloc" or something instead...and so on and so forth...C /
C++ isn't actually utilising any "magic" for this but simply
structures code in such a way as to try to keep the "OS specific"
parts separate from the "OS independent" bits...and then, during
linking, it selects the appropriate "OS specific" version of its
libraries...
The HLA package already has its "standard library" written for Windows
and Linux (and as other versions of HLA appear, the library will
likely get "ported" to those OSes too :)...and, with carefully written
code, it can actually manage what many think is "impossible" with
assembly...the same source code can be assembled to work for Windows
or Linux _without modification_...and the means it does this is, in
fact, exactly the same way C / C++ uses to do the same thing with its
"standard library" (where "carefully written code" in C / C++ terms
means sticking to only the ANSI-compliant header files because those
are "standardised" to be available with every C / C++ compiler :)...
HLA isn't unique in being able to do this...the only thing special
about this package is that Randy has already gone to the effort of
creating the "standard library" for Windows and Linux versions...but,
thinking of another assembler that can run on Windows and Linux - like
NASM - then the exact same approach could be used...the "magic"
doesn't reside in the compiler / assembler, in fact, but in the way
the code is _structured_ so, as long as you've got an assembler that
works on more than one OS (so that you can actually run it :) then
it's always possible to do this stuff...
Anyway, as you're coming from C++, then you'll find that the memory
allocation stuff is more or less the same, it's just that you directly
call the OS's memory API functions directly (which tend to be specific
to each OS)...or that you manipulate the stack yourself to create
local variables...and, well, other than syntax, there's no real
difference between "static" data declarations...
If you'd come from some other language with "garbage collection" and
so forth, then there would be the difference of getting used to not
having any such thing "automatically" working in the background...but,
with C++, it doesn't have that, anyway, so you won't miss that it's
not there, being accustomed to the idea of "you're _responsible_ for
that sort of thing yourself"...
And here ends today's lesson...tomorrow, we'll be dealing with quantum
uncertainity and the top 10 tips on how to host the perfect Christmas
office party (and knowing the way this group strays off-topic that
probably will turn out to be the case, even though I am, of course,
just kidding around here ;)...
Beth :)
- Next message: Robert Redelmeier: "Re: allocating memory."
- Previous message: Toby Thain: "Re: OT: Whiny CS Majors"
- In reply to: Jumbo: "allocating memory."
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]