Re: [Tutor] Windows Memory Basics
I am going to put your reply in a special place, for the day I can understand it :) On Tue, Oct 17, 2017 at 2:37 AM, James Chapman wrote: > We're heading into advanced territory here and I might get told off but... > Consider this C++ program for a second, it has a struct with different > types of variables which sit next to each other in memory. When you print > the byte values of the struct, you can see that there is no easy way to > know which byte value belongs to which variable unless you already know the > layout. > > - > #include > > typedef unsigned char BYTE; > typedef unsigned short WORD; > typedef unsigned long DWORD; > > #pragma pack(1) > struct mem > { > char c; // 1 byte > WORD wn;// 2 byte > DWORD dwn; // 4 byte > int i; // 4 byte > BYTE b; // 1 byte > }; > > int main() > { > mem s; > s.c = 0xFF; > s.wn = 0xF; > s.dwn = 0x; > s.i = 0x; > s.b = 0xFF; > > BYTE * memPointer = reinterpret_cast(&s); > > for (int i = 0; i < sizeof(mem); i++) > printf("%02d [0x%08x] = %x \n", i, memPointer, *memPointer++); > > return 0; > } > - > > Prints > > 00 [0xecf0f789] = ff > 01 [0xecf0f78a] = ff > 02 [0xecf0f78b] = ff > 03 [0xecf0f78c] = ff > 04 [0xecf0f78d] = ff > 05 [0xecf0f78e] = ff > 06 [0xecf0f78f] = ff > 07 [0xecf0f790] = ff > 08 [0xecf0f791] = ff > 09 [0xecf0f792] = ff > 10 [0xecf0f793] = ff > 11 [0xecf0f794] = ff > > Packing can also come into play. If you change the packing to 2, you get > this: > > 00 [0x7d4ffcd9] = ff > 01 [0x7d4ffcda] = cc > 02 [0x7d4ffcdb] = ff > 03 [0x7d4ffcdc] = ff > 04 [0x7d4ffcdd] = ff > 05 [0x7d4ffcde] = ff > 06 [0x7d4ffcdf] = ff > 07 [0x7d4ffce0] = ff > 08 [0x7d4ffce1] = ff > 09 [0x7d4ffce2] = ff > 10 [0x7d4ffce3] = ff > 11 [0x7d4ffce4] = ff > 12 [0x7d4ffce5] = ff > 13 [0x7d4ffce6] = cc > > And if you change it to 4, you get this: > > 00 [0xf4f5fbf9] = ff > 01 [0xf4f5fbfa] = cc > 02 [0xf4f5fbfb] = ff > 03 [0xf4f5fbfc] = ff > 04 [0xf4f5fbfd] = ff > 05 [0xf4f5fbfe] = ff > 06 [0xf4f5fbff] = ff > 07 [0xf4f5fc00] = ff > 08 [0xf4f5fc01] = ff > 09 [0xf4f5fc02] = ff > 10 [0xf4f5fc03] = ff > 11 [0xf4f5fc04] = ff > 12 [0xf4f5fc05] = ff > 13 [0xf4f5fc06] = cc > 14 [0xf4f5fc07] = cc > 15 [0xf4f5fc08] = cc > > > In other words, even if you have the source code for the program you want > to scan in memory, depending on the compiler settings the memory layout > could have changed, or rather not be what you expected due to packing and > alignment. > > Probably not the answer you were hoping for but I hope this helps. > > -- > James > > > > > On 17 October 2017 at 01:02, Michael C > wrote: > >> Hold on, supposed by using Openprocess and VirtualQueryEx, I have the >> locations of all the memory the application is using, wouldn't this to be >> true? >> >> Say, a 8 byte data is somewhere in the region i am scanning. Ok, I know by >> scanning it like this >> for n in range(start,end,1) >> >> will read into another variable and mostly nothing, but unless a variable, >> that is, one number, can be truncated and exist in multiple locations like >> this >> >> double = 12345678 >> >> 123 is at x001 >> 45 is at x005 >> 678 is at x010 >> >> unless a number can be broken up like that, wouldn't I, while use the >> silly >> 'increment by one' approach, actually luck out and get that value in it's >> actual position? >> >> ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor
Re: [Tutor] Windows Memory Basics
cool stuff! On Tue, Oct 17, 2017 at 2:17 AM, Alan Gauld via Tutor wrote: > On 17/10/17 01:02, Michael C wrote: > > > that is, one number, can be truncated and exist in multiple locations > like > > this > > > > double = 12345678 > > > > 123 is at x001 > > 45 is at x005 > > 678 is at x010 > > That won't happen, a single variable will always be in a a single > area. > > But the representation won't be anything like you suggested. A > single number 12345678(assuming its a decimal integer) will be > stored as 0xbc614e, which is 3 bytes, so it will be part of > a 4byte (assuming a 32bit integer) chunk of storage. > Of course if the program declared the variable to be a long > then the same 3 bytes will be stored within an 8 byte chunk. > And if it was stored as a double floating point value then > the byte representation will be entirely different (and > I don't even know what that would be). > > > unless a number can be broken up like that, wouldn't I, > > while use the silly 'increment by one' approach, > > actually luck out and get that value in it's actual position? > > Yes, if you know that the decimal number 12345678 is stored > somewhere in memory, you can scan looking for the 3 bytes 0xbc, > 0x61,0x4e. And if you also know it was stored in a 32 bit int > you can check for zero before the first byte (or second, or last) > depending on the endian storage system used by your OS). > > But you still don't know for sure that you didn't just find a > byte of 0xbc followed by the start of a UTF8 string beginning > with the characters 'aN'... And there are likely to be several > hits not just one. You need to figure out which are your number > and which are just groups of 3 bytes that happen to look like it. > > If you are very clever you can look at the data surrounding > each set of bytes and make a fair guess about ones which > are not likely to be your variable (as above you might look > to see if the following bytes are all viable ascii characters > which might indicate that it was indeed a string and not > your number). But that may still leave several candidates, > and its all fraught with difficulty. > > If you do know the data types involved you can read your > memory into a buffer and apply the struct module to > interpret it (possibly in multiple ways) to extract > the values but you must know the nature of what you are > reading to be able to interpret it. ie. you need to know > the types. > > Reading the bytes in memory is one thing, and relatively easy. > Interpreting those bytes as actual data is nigh impossible > unless you know in advance what data types you are looking at. > > -- > Alan G > Author of the Learn to Program web site > http://www.alan-g.me.uk/ > http://www.amazon.com/author/alan_gauld > Follow my photo-blog on Flickr at: > http://www.flickr.com/photos/alangauldphotos > > > ___ > Tutor maillist - Tutor@python.org > To unsubscribe or change subscription options: > https://mail.python.org/mailman/listinfo/tutor > ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor
Re: [Tutor] Windows Memory Basics
We're heading into advanced territory here and I might get told off but... Consider this C++ program for a second, it has a struct with different types of variables which sit next to each other in memory. When you print the byte values of the struct, you can see that there is no easy way to know which byte value belongs to which variable unless you already know the layout. - #include typedef unsigned char BYTE; typedef unsigned short WORD; typedef unsigned long DWORD; #pragma pack(1) struct mem { char c; // 1 byte WORD wn;// 2 byte DWORD dwn; // 4 byte int i; // 4 byte BYTE b; // 1 byte }; int main() { mem s; s.c = 0xFF; s.wn = 0xF; s.dwn = 0x; s.i = 0x; s.b = 0xFF; BYTE * memPointer = reinterpret_cast(&s); for (int i = 0; i < sizeof(mem); i++) printf("%02d [0x%08x] = %x \n", i, memPointer, *memPointer++); return 0; } - Prints 00 [0xecf0f789] = ff 01 [0xecf0f78a] = ff 02 [0xecf0f78b] = ff 03 [0xecf0f78c] = ff 04 [0xecf0f78d] = ff 05 [0xecf0f78e] = ff 06 [0xecf0f78f] = ff 07 [0xecf0f790] = ff 08 [0xecf0f791] = ff 09 [0xecf0f792] = ff 10 [0xecf0f793] = ff 11 [0xecf0f794] = ff Packing can also come into play. If you change the packing to 2, you get this: 00 [0x7d4ffcd9] = ff 01 [0x7d4ffcda] = cc 02 [0x7d4ffcdb] = ff 03 [0x7d4ffcdc] = ff 04 [0x7d4ffcdd] = ff 05 [0x7d4ffcde] = ff 06 [0x7d4ffcdf] = ff 07 [0x7d4ffce0] = ff 08 [0x7d4ffce1] = ff 09 [0x7d4ffce2] = ff 10 [0x7d4ffce3] = ff 11 [0x7d4ffce4] = ff 12 [0x7d4ffce5] = ff 13 [0x7d4ffce6] = cc And if you change it to 4, you get this: 00 [0xf4f5fbf9] = ff 01 [0xf4f5fbfa] = cc 02 [0xf4f5fbfb] = ff 03 [0xf4f5fbfc] = ff 04 [0xf4f5fbfd] = ff 05 [0xf4f5fbfe] = ff 06 [0xf4f5fbff] = ff 07 [0xf4f5fc00] = ff 08 [0xf4f5fc01] = ff 09 [0xf4f5fc02] = ff 10 [0xf4f5fc03] = ff 11 [0xf4f5fc04] = ff 12 [0xf4f5fc05] = ff 13 [0xf4f5fc06] = cc 14 [0xf4f5fc07] = cc 15 [0xf4f5fc08] = cc In other words, even if you have the source code for the program you want to scan in memory, depending on the compiler settings the memory layout could have changed, or rather not be what you expected due to packing and alignment. Probably not the answer you were hoping for but I hope this helps. -- James On 17 October 2017 at 01:02, Michael C wrote: > Hold on, supposed by using Openprocess and VirtualQueryEx, I have the > locations of all the memory the application is using, wouldn't this to be > true? > > Say, a 8 byte data is somewhere in the region i am scanning. Ok, I know by > scanning it like this > for n in range(start,end,1) > > will read into another variable and mostly nothing, but unless a variable, > that is, one number, can be truncated and exist in multiple locations like > this > > double = 12345678 > > 123 is at x001 > 45 is at x005 > 678 is at x010 > > unless a number can be broken up like that, wouldn't I, while use the silly > 'increment by one' approach, actually luck out and get that value in it's > actual position? > > ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor
Re: [Tutor] Windows Memory Basics
On 17/10/17 01:02, Michael C wrote: > that is, one number, can be truncated and exist in multiple locations like > this > > double = 12345678 > > 123 is at x001 > 45 is at x005 > 678 is at x010 That won't happen, a single variable will always be in a a single area. But the representation won't be anything like you suggested. A single number 12345678(assuming its a decimal integer) will be stored as 0xbc614e, which is 3 bytes, so it will be part of a 4byte (assuming a 32bit integer) chunk of storage. Of course if the program declared the variable to be a long then the same 3 bytes will be stored within an 8 byte chunk. And if it was stored as a double floating point value then the byte representation will be entirely different (and I don't even know what that would be). > unless a number can be broken up like that, wouldn't I, > while use the silly 'increment by one' approach, > actually luck out and get that value in it's actual position? Yes, if you know that the decimal number 12345678 is stored somewhere in memory, you can scan looking for the 3 bytes 0xbc, 0x61,0x4e. And if you also know it was stored in a 32 bit int you can check for zero before the first byte (or second, or last) depending on the endian storage system used by your OS). But you still don't know for sure that you didn't just find a byte of 0xbc followed by the start of a UTF8 string beginning with the characters 'aN'... And there are likely to be several hits not just one. You need to figure out which are your number and which are just groups of 3 bytes that happen to look like it. If you are very clever you can look at the data surrounding each set of bytes and make a fair guess about ones which are not likely to be your variable (as above you might look to see if the following bytes are all viable ascii characters which might indicate that it was indeed a string and not your number). But that may still leave several candidates, and its all fraught with difficulty. If you do know the data types involved you can read your memory into a buffer and apply the struct module to interpret it (possibly in multiple ways) to extract the values but you must know the nature of what you are reading to be able to interpret it. ie. you need to know the types. Reading the bytes in memory is one thing, and relatively easy. Interpreting those bytes as actual data is nigh impossible unless you know in advance what data types you are looking at. -- Alan G Author of the Learn to Program web site http://www.alan-g.me.uk/ http://www.amazon.com/author/alan_gauld Follow my photo-blog on Flickr at: http://www.flickr.com/photos/alangauldphotos ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor
Re: [Tutor] Windows Memory Basics
On 17/10/17 00:53, Michael C wrote: > ah, i am bummed completely haha. > > Is there a way to tell which parts a variables so I can scan it? > Maybe you could point me to some reading materials? There are some rules about where programs store data within their memory space, but typically that will only give you the start address of a data area. It still doesn't give you any clue as to what is stored in that area in terms of data types. As to reading material there are several books on OS that you could try. One of the easiest and shortest is "Fundamentals of OS" by Lister. But because its general in nature it won;t help with Windows specifics. For windows specifics its back to MSDN, but that is not very accessible. -- Alan G Author of the Learn to Program web site http://www.alan-g.me.uk/ http://www.amazon.com/author/alan_gauld Follow my photo-blog on Flickr at: http://www.flickr.com/photos/alangauldphotos ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor
Re: [Tutor] Windows Memory Basics
Hold on, supposed by using Openprocess and VirtualQueryEx, I have the locations of all the memory the application is using, wouldn't this to be true? Say, a 8 byte data is somewhere in the region i am scanning. Ok, I know by scanning it like this for n in range(start,end,1) will read into another variable and mostly nothing, but unless a variable, that is, one number, can be truncated and exist in multiple locations like this double = 12345678 123 is at x001 45 is at x005 678 is at x010 unless a number can be broken up like that, wouldn't I, while use the silly 'increment by one' approach, actually luck out and get that value in it's actual position? On Mon, Oct 16, 2017 at 4:53 PM, Michael C wrote: > ah, i am bummed completely haha. > > Is there a way to tell which parts a variables so I can scan it? > Maybe you could point me to some reading materials? > > thanks :) > > On Mon, Oct 16, 2017 at 4:48 PM, Alan Gauld via Tutor > wrote: > >> On 16/10/17 21:04, Michael C wrote: >> >> > I don't understand this part about the memory: >> >> And I'm not sure I understand your question but... >> >> > if I used VirtualQueryEx to find out if a region of memory is ok to >> scan, >> > and it >> > says it's ok, are the values in the region arranged like this: >> > >> > short,int,double,long,char, double, short in >> > >> > as in, random? >> >> They won't be random, they'll be in the order that the >> program that wrote the memory chose them to be in. For >> example the memory might contain some program variables >> and those variables may be of different types (assuming >> a compiled language like C++, say). Or it may be holding >> a complex data structure, like a class, that has fields >> of different types. >> >> What those types are will not be obvious and unless you >> know what you are reading will be impossible to guess >> in most cases since it is just a sequence of bytes and >> one set of 8 bits looks a lot like any other. >> >> > I am asking this because, if it's random, then I'd have to run >> > ReadProcessMemory >> > by increasing the value of of my loop by ONE (1) at a time, like this >> >> That doesn't really help, you need to know what each >> chunk of data represents and then increment the index >> by the size of each corresponding data type. >> >> For example if you have a string of 8 UTF8 characters >> that will probably be 8 bytes long(some UTF characters >> are more than 8 bits). But those 8 bytes could equally >> be a floating point number or a long integer or a >> struct containing 2 32 bit ints. You have absolutely >> no way to tell. >> >> And if you increment your index by one you will then >> look at the first 7 bytes plus one other. What is >> the 8th byte? It could be the start of another float, >> another UTF8 character or something else entirely. >> >> Things are then further complicated by the tendency >> to store data on word boundaries, so either 4 or >> 8 byte chunks, but even that can't be guaranteed >> since it could be a compressed memory scheme in >> action or a piece of assembler code taking the >> 'law' into its own hands. >> >> And of course it may not represent anything since >> many programs set aside memory spaqce for later use >> and either fill it with zeros or some other arbitrary >> pattern, or just leave it with whatever bits happened >> to already be there. >> >> > for i in range(start_of_region, end_of_region, 1): >> > ReadProcessMemory(Process, i, ctypes.byref(buffer), >> > ctypes.sizeof(buffer), ctypes.byref(nread)) >> > >> > Is that correct? >> >> Probably not. If you know what data you are reading you >> can do what you want, but if it's just a random block >> of memory you are scanning then its almost impossible >> to determine for certain what the raw data represents. >> >> If you have access to a *nix system (or cygwin >> on windows) it may help you to see the nature >> of the problem by running od -x on a text file >> You can find out what is in it by looking at it >> in a text editor but the hex listing will be >> meaningless. If that's what simple text looks >> like imagine what a binary file containing >> mixed data is like. >> >> -- >> Alan G >> Author of the Learn to Program web site >> http://www.alan-g.me.uk/ >> http://www.amazon.com/author/alan_gauld >> Follow my photo-blog on Flickr at: >> http://www.flickr.com/photos/alangauldphotos >> >> >> ___ >> Tutor maillist - Tutor@python.org >> To unsubscribe or change subscription options: >> https://mail.python.org/mailman/listinfo/tutor >> > > ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor
Re: [Tutor] Windows Memory Basics
ah, i am bummed completely haha. Is there a way to tell which parts a variables so I can scan it? Maybe you could point me to some reading materials? thanks :) On Mon, Oct 16, 2017 at 4:48 PM, Alan Gauld via Tutor wrote: > On 16/10/17 21:04, Michael C wrote: > > > I don't understand this part about the memory: > > And I'm not sure I understand your question but... > > > if I used VirtualQueryEx to find out if a region of memory is ok to scan, > > and it > > says it's ok, are the values in the region arranged like this: > > > > short,int,double,long,char, double, short in > > > > as in, random? > > They won't be random, they'll be in the order that the > program that wrote the memory chose them to be in. For > example the memory might contain some program variables > and those variables may be of different types (assuming > a compiled language like C++, say). Or it may be holding > a complex data structure, like a class, that has fields > of different types. > > What those types are will not be obvious and unless you > know what you are reading will be impossible to guess > in most cases since it is just a sequence of bytes and > one set of 8 bits looks a lot like any other. > > > I am asking this because, if it's random, then I'd have to run > > ReadProcessMemory > > by increasing the value of of my loop by ONE (1) at a time, like this > > That doesn't really help, you need to know what each > chunk of data represents and then increment the index > by the size of each corresponding data type. > > For example if you have a string of 8 UTF8 characters > that will probably be 8 bytes long(some UTF characters > are more than 8 bits). But those 8 bytes could equally > be a floating point number or a long integer or a > struct containing 2 32 bit ints. You have absolutely > no way to tell. > > And if you increment your index by one you will then > look at the first 7 bytes plus one other. What is > the 8th byte? It could be the start of another float, > another UTF8 character or something else entirely. > > Things are then further complicated by the tendency > to store data on word boundaries, so either 4 or > 8 byte chunks, but even that can't be guaranteed > since it could be a compressed memory scheme in > action or a piece of assembler code taking the > 'law' into its own hands. > > And of course it may not represent anything since > many programs set aside memory spaqce for later use > and either fill it with zeros or some other arbitrary > pattern, or just leave it with whatever bits happened > to already be there. > > > for i in range(start_of_region, end_of_region, 1): > > ReadProcessMemory(Process, i, ctypes.byref(buffer), > > ctypes.sizeof(buffer), ctypes.byref(nread)) > > > > Is that correct? > > Probably not. If you know what data you are reading you > can do what you want, but if it's just a random block > of memory you are scanning then its almost impossible > to determine for certain what the raw data represents. > > If you have access to a *nix system (or cygwin > on windows) it may help you to see the nature > of the problem by running od -x on a text file > You can find out what is in it by looking at it > in a text editor but the hex listing will be > meaningless. If that's what simple text looks > like imagine what a binary file containing > mixed data is like. > > -- > Alan G > Author of the Learn to Program web site > http://www.alan-g.me.uk/ > http://www.amazon.com/author/alan_gauld > Follow my photo-blog on Flickr at: > http://www.flickr.com/photos/alangauldphotos > > > ___ > Tutor maillist - Tutor@python.org > To unsubscribe or change subscription options: > https://mail.python.org/mailman/listinfo/tutor > ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor
Re: [Tutor] Windows Memory Basics
On Mon, Oct 16, 2017 at 01:04:40PM -0700, Michael C wrote: > Hi all: > > > I don't understand this part about the memory: > > if I used VirtualQueryEx to find out if a region of memory is ok to scan, > and it > says it's ok, are the values in the region arranged like this: > > short,int,double,long,char, double, short in > > as in, random? I am not a Windows expert, but I doubt it. Memory is always an array of bytes. How you interpret that memory depends on what you are doing with it, and there's no way to tell from outside how it should be interpreted. (Some very clever, perhaps too clever, can even give the same chunk of memory two or more *valid* interpretations at the same time.) This implies that unless you know that this chunk of memory has some special meaning to Windows, the choice of how to interpret the chunk of memory is up to you. If you have a block of memory in hexadecimal that looks like this: 1f78a924b6c00be4f7546cda298860951a6c30d75640e62f82c8f5c0f1cb0bfc then it is entirely up to you whether you interpret it as: (1) 64 one-byte values: 1f 78 a9 24 ... (2) 32 two-byte values: 1f78 a924 b6c0 ... (3) 16 four-byte values: 1f78a924 b6c00be4 ... or something else. You could interpret them as ASCII bytes, Unicode code points, signed integers, unsigned integers, single- or double-precision floating point numbers, strings, or anything you like. -- Steve ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor
Re: [Tutor] Windows Memory Basics
On 16/10/17 21:04, Michael C wrote: > I don't understand this part about the memory: And I'm not sure I understand your question but... > if I used VirtualQueryEx to find out if a region of memory is ok to scan, > and it > says it's ok, are the values in the region arranged like this: > > short,int,double,long,char, double, short in > > as in, random? They won't be random, they'll be in the order that the program that wrote the memory chose them to be in. For example the memory might contain some program variables and those variables may be of different types (assuming a compiled language like C++, say). Or it may be holding a complex data structure, like a class, that has fields of different types. What those types are will not be obvious and unless you know what you are reading will be impossible to guess in most cases since it is just a sequence of bytes and one set of 8 bits looks a lot like any other. > I am asking this because, if it's random, then I'd have to run > ReadProcessMemory > by increasing the value of of my loop by ONE (1) at a time, like this That doesn't really help, you need to know what each chunk of data represents and then increment the index by the size of each corresponding data type. For example if you have a string of 8 UTF8 characters that will probably be 8 bytes long(some UTF characters are more than 8 bits). But those 8 bytes could equally be a floating point number or a long integer or a struct containing 2 32 bit ints. You have absolutely no way to tell. And if you increment your index by one you will then look at the first 7 bytes plus one other. What is the 8th byte? It could be the start of another float, another UTF8 character or something else entirely. Things are then further complicated by the tendency to store data on word boundaries, so either 4 or 8 byte chunks, but even that can't be guaranteed since it could be a compressed memory scheme in action or a piece of assembler code taking the 'law' into its own hands. And of course it may not represent anything since many programs set aside memory spaqce for later use and either fill it with zeros or some other arbitrary pattern, or just leave it with whatever bits happened to already be there. > for i in range(start_of_region, end_of_region, 1): > ReadProcessMemory(Process, i, ctypes.byref(buffer), > ctypes.sizeof(buffer), ctypes.byref(nread)) > > Is that correct? Probably not. If you know what data you are reading you can do what you want, but if it's just a random block of memory you are scanning then its almost impossible to determine for certain what the raw data represents. If you have access to a *nix system (or cygwin on windows) it may help you to see the nature of the problem by running od -x on a text file You can find out what is in it by looking at it in a text editor but the hex listing will be meaningless. If that's what simple text looks like imagine what a binary file containing mixed data is like. -- Alan G Author of the Learn to Program web site http://www.alan-g.me.uk/ http://www.amazon.com/author/alan_gauld Follow my photo-blog on Flickr at: http://www.flickr.com/photos/alangauldphotos ___ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor