arm | 代写assembly | Python代写 | 代做lab  | C代写 – Lab 9 Your own printf() function

Lab 9 Your own printf() function

arm | 代写assembly | Python代写 | 代做lab  | C代写 – 该题目是一个汇编的练习题目代写, 是比较典型的arm/assembly/Python/c语言等代写方向, 该题目是值得借鉴的lab代写的题目,是比较有意义的C和arm汇编的转换题目。

lab代写 代写lab

1. Learning Objectives

  1. Gain further experience with programming in assembly language.
  2. Use the stack for dynamic allocation and deallocation of data storage space.
  3. Explore the conversions between data representations used in the processor circuitry for characters, strings, and integers and the ASCII characters used to represent that data when it is output to a machine user.

2. Introduction

Print to the terminal is among the most basic functions that an operating system provides. Before graphical user interfaces, people interacted with computers via the terminal and only the terminal. In many cases, terminals were able to display only ASCII characters.

Print to the terminal functionality begins with the ability to print a single character. Printing arbitrary multiple-character output is accomplished by controlled use of the single character mechanism multiple times. Such a mechanism is computer specific and part of the operating system.

For this lab we will use the C language single character printing mechanism putchar() every time we have a single character to print to the terminal. putchar() is the only output function that you may call in this lab.

putchar() takes an argument that it treats as an ASCII code and sends that ASCII code, transformed as needed, to the standard output hardware device for the computer, as assigned by the operating system. For example, if a computer with a graphical processor unit (GPU) is using a flat panel display as its standard output device, then putchar(‘a’) transforms the ASCII code for a into the a character of the current application font and font size, looks up the transformation of that character into a pixel map, and writes that pixel map into the appropriate location within the frame buffer memory of the GPU so that character a appears on the screen where intended.

putchar(), used repetitively, can output all the characters we may wish to the computer display. The challenge much of the time is computing the correct ASCII code to provide as the argument for putchar() given the variety of data and number representation schemes used in programs and in processor hardware. This conversion is the focus of this lab.

3. Variadic Functions

The <stdarg.h> header file helps with variadic functions by declaring the va_list type as well as parsing functions. Do not use it. You are to come up with your own solution.

We often use the function printf() like this:

printf(“Elapsed time clocks: %d\n”, clock() – time_start);

Sometimes we may pass many arguments to it:

// It’s really hard to debug OS code // so debug printf’s are everywhere. printf(“The process selected for execution is #%d: ‘%s’ with a priority of %d\n”, currpid, prptr->prname, prptrprprio);

Interestingly, printf() can accept an arbitrary number of arguments. This means that printf() is an example of a variadic function.

The function prototype for printf() is specified as follows: (you can see it on the man page by typing man 3 printf in the terminal):

int printf(const char *format, …);

The first argument format is always a string literal in ASCII. The remaining list of arguments, of which there can be zero or more, is specified as …. These are the values to be converted to ASCII according to each specifier contained in the format string literal. Showing three periods as an argument is the way to specify that a function is variadic.

The first question to answer when implementing any variadic function, such as printf(), is how do you reference the second argument and beyond? There is no explicit variable named for each, so we will have to directly reference their values on the stack. To do so, we must leverage a key aspect of the calling convention: arguments that are passed to a function are pushed onto the stack in reverse order, that is, the last argument is the first to be pushed on the stack.

Thus, the first argument for printf(), the format string literal, will be the last argument pushed onto the stack and, thus, the first argument accessed when the arguments are popped from the stack. Recall also that new arguments are pushed on to the stack at successively lesser magnitude addresses because the stack grows towards address zero. This means the format string will be located at the lowest numbered address, while the other arguments of printf() will occupy successively greater magnitude addresses in the order in which they appear. Put another way, when an item is pushed onto the stack, the address stored at the address pointed to by the bit string in the stack pointer register, and then the stack pointer address, sp, is decremented. When an item is popped, the address in sp increases. Once we know the location of the first argument and the size of all arguments, we know the starting points (byte addresses) for all arguments.

Constant (fixed value) string arguments, such as the format string for the printf() function, are not stored in the stack. Instead, they are stored in the data segment of a program, which lies far from the stack in much lower memory addresses near the text segment. So, what is pushed onto the stack for a static string is not its value, but a pointer to the start of the string in the data segment.

You may have noticed that this argument passing convention disagrees with the arm convention from an earlier lab. For that ARM convention, the first up to four arguments were passed via registers r0 to r3. Any additional arguments were passed via the stack. This convention leads to inconsistent handling of arguments for variadic functions, however. Therefore, the C compiler places all arguments for a

variadic function on the stack. As an aside, the C compiler will also place all arguments on the stack if at any point the program refers to the address of an argument.

4. Conversion Specifiers

The format string used by printf() directs what is to be printed. The format string may contain literal characters to print, and it may also contain conversion specifiers, which are string literals of two or more characters that begin with the character %. Some conversion specifiers are listed in the following.

  • %c is the conversion specifier for a character.
  • %s specifies conversion of a character string ending in 0x0 0 , or NULL.
  • %x specifies an integer to be printed in hexadecimal representation using lower case letters.
  • %d indicates conversion of a binary integer to the ASCII string for its decimal representation.
  • %f is for conversion from IEEE 754 to the ASCII string of its decimal scientific equivalent.

The standard printf() function supports additional specifiers and modifiers. An example of a modified specifier is %5.2f, which directs the display of a decimal floating point number (a float) using a minimum field width of a total of 5 characters with two decimal digits to the right of the decimal point and the use of 0 digits to pad to the minimum width, if needed. We will ignore such complex specifiers for this lab.

For this lab, we will implement the following conversion specifiers and the specifier escape. All of these conversions produce ASCII character(s) that will be given one at a time to putchar() for printing.

  • %c, single character
  • %s, character string
  • %x, hexadecimal using lower case
  • %X, hexadecimal using upper case
  • %d, signed decimal integer
  • %%, this indicates as escape from conversion to allow printing of the % character.

You may assume that no illegal directives will be present in the format string given to your code. A compiler would terminate its work if an illegal format directive for printf() were found, preventing illegal directives in actual use.

5. Implementation

You should name your C language print to the terminal function myprintf(), so it does not conflict with the standard printf(). The prototype for your function is:

int myprintf(const char * format, …);

Your function should locate the first argument on the stack, iterate through the individual characters in the format string and, using putchar(), output every character that is not a conversion specifier. When a conversion specifier is encountered, that is, a percent (%), you should locate the next argument on the stack. Then, again using putchar() for each character, output that argument after converting it, as necessary and according to the specifier, from the representation within your Pis circuitry to a

sequence of ASCII characters for display on the terminal. If two consecutive % symbols are encountered (%%) this means that the % symbol is being escaped from its usual meaning as the start of a conversion specifier so that the % symbol itself is possible to print to the terminal. In this case, the argument is not on the stack, and you should use putchar() to output a single percent sign character.

5 .1 Character and string conversion After %%, %c is the simplest conversion specifier. For %c, just find the corresponding argument on the stack and pass it to putchar().

Printing a character string is a slight extension to %c. Find the corresponding pointer argument on the stack and, starting at that address, repeatedly call putchar() on each successive character until you encounter the string terminator, which is a byte containing \0, also known as NULL.

5 .2 Hexadecimal conversion When outputting the hexadecimal equivalent for an integer, assume that leading 0s should be omitted. printf() supports padding with leading zeros, but we will not implement support for this.

Do not use any global variables in your printx() implementation.

For the %x conversion specifier, write a function, printx.s, in ARM assembly that takes a 32-bit integer as the argument and generates the appropriate ASCII character sequence. You can then call printx() from your myprintf() C function when you encounter %x. You must also permit printx() to take a second argument to specify uppercase to implement the %X conversion.

Notice that the hexadecimal-represented number using no leading zeros may have anywhere from 1 to 8 digits. This means that when parsing, starting with the MSB could result in the output of leading zeros. Instead, start with the LSB.

First, think about which pair of ARM logical operations can be used together to clear (set to 0) all but the rightmost 4 bits of the provided integer. Shifting in zeros is a way to clear bits in a register. Then, think about how to convert the resulting integer, call it Z, into the ASCII character code corresponding to the representation of Z within the 16-symbol hexadecimal representation system, {0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f}. Keep in mind that putchar() will interpret the argument passed to it using ASCII encoding. If you call putchar(9), the terminal will not display a 9 character. Instead, a tab character will be printed, per the ASCII encoding.

Finally, consider where to store the ASCII character you generate until it is time to retrieve it for use as an argument for putchar(). Registers are not a good choice for storing these ASCII characters because the integer will have an unknown number of digits. The expandable stack in main memory is a good choice. Push and pop instructions will be extremely helpful because the last ASCII character pushed onto the stack will be the first one popped off to be printed.

5 .3 Decimal integer conversion and the need to divide by 10 Write a function, printd.s, in ARM assembly to convert 2s complement binary representation to decimal integer represented as an ASCII character string. Like printx(), printd() should take a single argument, a 32-bit signed integer. You may not use any global variables in printd().

Printing an integer in decimal form is similar to hexadecimal, but there is another complication: signed integers are stored in 2s complement representation. Unlike hexadecimal, there is no straightforward way to get the decimal representation just by looking at groupings of bits. What to do?

Twos complement representation and human-friendly sign-magnitude decimal representation are weighted positional representations. Let the Pi contain an integer, N, in 2s complement, which means Bit_String_for_N = bk- 1 x 2k-^1 + bk- 2 x 2k-^2 + … + b 1 x 2^1 + b 0 x 2^0 [Eqn. 1]

The negative sign on the initial term of this summation in Equation 1 is what makes 2s complement work so well with addition of negative integers. The 2s complement encoding of all representable integers is purely weighted positional, including for negative integers, unlike the encoding of negative numbers in 1s complement representation.

We want to output N in sign-magnitude decimal weighted positional form like this Character_string_for _sign-magnitude_decimal_weighted_N = <Sign, dj- 1 , dj- 2 , … , d 1 , d 0 > where |N| = dj- 1 x 10j-^1 + dj- 2 x 10j-^2 + … + d 1 x 10^1 + d 0 x 10^0 [Eqn. 2]

Division of an integer N by an integer divisor, D, is defined as N = Q*D + R, where 0 |R| < |D| [Eqn. 3]

Re-grouping Equation 2 into the two terms of Equation 3, and noting that 10^0 = 1, yields |N| = Q10 + R = {dj- 1 x 10j-^2 + dj- 2 x 10j-^3 + … + d 1 x 10^0 }10 + d 0 [Eqn. 4]

The value of the remainder, the least significant digit of N, can be extracted to a register in your Pi by using the equation Least_significant_decimal_digit_of_N = d 0 = |N| Q*10 [Eqn. 5]

Extracting Q in Equation 4, we have |Q| = dj- 1 x 10j-^2 + dj- 2 x 10j-^3 + … + d 2 x 10^1 + d 1 x 10^0 [Eqn. 6]

A sequence N, Q 1 , Q 2 , …, Qj that ends when Qj = 0 can be generated by repeated division of each successive quotient by 10. Each Qi can be used to deliver the next more significant digit, di, of the base 10 representation of N using the method of Equation 5.

Finally, if N is a negative integer, then a sign character can be pre-pended to the sequence of decimal digits thus far generated.

There is no native instruction for integer division in ARMv7. There are some pseudo-instructions given in the ARM manual, but they are relatively complicated and represent a set of instructions that unpack the division operation using a loop.

Fortunately, because the divisor for our purposes is always 10, we can use a mathematical relationship discovered long ago for dividing by a constant. There are many articles that discuss this approach. One of them can be found here under Division by a constant integer. There is an associated tool to generate the instructions to perform the division as well. Copy the Python code to a file on your Pi, and

invoke it using python . Make sure that you understand how to use this code.

Create a single assembly language program named printinteger.s to hold your printx.s, printd.s, and the divide-by-10 assembly code in one convenient package.

5.4 Test Cases

The zip file that you download contains a Makefile and two test files, main.c and twelvedays.c, that will call your myprintf.c implementation to check its functionality, and mainout and twelvedaysout that contain a copy of the output that should be produced by main.c and twelvedays.c in conjunction with your programs.

5 .5 Corner Cases Keep in mind the following corner cases.

  • What if the integer to print is 0?
  • What if the integer to print is negative? Remember, the most significant bit of the 2s complement representation will be 1.

6. Grading

[40 points] Demonstrate your myprintf( ) running on your Pi to your TA and producing correct output with main.c and twelvedays.c.

[ 3 0 points] Answer questions from your TA during your code demonstration.

[30 points] Before your lab session starts, submit your two code files, myprintf.c, which implements the myprintf() function. printinteger.s, which contains the ARM assembly code for printx() and printd(). to for the Lab 9 assignment.